/tortoisehg/hgtk/datamine.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 875 lines · 721 code · 102 blank · 52 comment · 96 complexity · 9606a7bf6062f21b157913d5480b3d4f MD5 · raw file

  1. # datamine.py - Data Mining dialog for TortoiseHg
  2. #
  3. # Copyright 2008 Steve Borho <steve@borho.org>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2, incorporated herein by reference.
  7. import gtk
  8. import gobject
  9. import os
  10. import pango
  11. import Queue
  12. import threading
  13. import re
  14. from mercurial import util, error
  15. from tortoisehg.util.i18n import _
  16. from tortoisehg.util import hglib, thread2
  17. from tortoisehg.util.colormap import AnnotateColorMap
  18. from tortoisehg.util.colormap import AnnotateColorSaturation
  19. from tortoisehg.hgtk.logview.treeview import TreeView as LogTreeView
  20. from tortoisehg.hgtk.logview import treemodel as LogTreeModelModule
  21. from tortoisehg.hgtk import gtklib, gdialog, changeset, statusbar, csinfo
  22. # Column indexes for grep
  23. GCOL_REVID = 0
  24. GCOL_LINE = 1 # matched line
  25. GCOL_DESC = 2 # utf-8, escaped summary
  26. GCOL_PATH = 3
  27. # Column indexes for annotation
  28. ACOL_REVID = 0
  29. ACOL_LINE = 1 # file line
  30. ACOL_DESC = 2 # utf-8, escaped summary
  31. ACOL_PATH = 3
  32. ACOL_COLOR = 4
  33. ACOL_USER = 5
  34. ACOL_LNUM = 6 # line number
  35. class DataMineDialog(gdialog.GWindow):
  36. def get_title(self):
  37. return _('%s - datamine') % self.get_reponame()
  38. def get_icon(self):
  39. return 'menurepobrowse.ico'
  40. def parse_opts(self):
  41. pass
  42. def get_tbbuttons(self):
  43. self.stop_button = self.make_toolbutton(gtk.STOCK_STOP, _('Stop'),
  44. self.stop_current_search,
  45. tip=_('Stop operation on current tab'))
  46. return [
  47. self.make_toolbutton(gtk.STOCK_FIND, _('New Search'),
  48. self.search_clicked,
  49. tip=_('Open new search tab')),
  50. self.stop_button
  51. ]
  52. def prepare_display(self):
  53. root = self.repo.root
  54. cf = []
  55. for f in self.pats:
  56. try:
  57. if os.path.isfile(f):
  58. cf.append(util.canonpath(root, self.cwd, f))
  59. elif os.path.isdir(f):
  60. for fn in os.listdir(f):
  61. fname = os.path.join(f, fn)
  62. if not os.path.isfile(fname):
  63. continue
  64. cf.append(util.canonpath(root, self.cwd, fname))
  65. except util.Abort:
  66. pass
  67. for f in cf:
  68. self.add_annotate_page(f, '.')
  69. if not self.notebook.get_n_pages():
  70. self.add_search_page()
  71. os.chdir(root)
  72. def save_settings(self):
  73. settings = gdialog.GWindow.save_settings(self)
  74. return settings
  75. def load_settings(self, settings):
  76. gdialog.GWindow.load_settings(self, settings)
  77. self.tabwidth = hglib.gettabwidth(self.repo.ui)
  78. def get_body(self):
  79. """ Initialize the Dialog. """
  80. self.grep_cmenu = self.grep_context_menu()
  81. self.changedesc = {}
  82. self.newpagecount = 1
  83. self.currev = None
  84. vbox = gtk.VBox()
  85. notebook = gtk.Notebook()
  86. notebook.set_tab_pos(gtk.POS_TOP)
  87. notebook.set_scrollable(True)
  88. notebook.popup_enable()
  89. notebook.show()
  90. self.notebook = notebook
  91. vbox.pack_start(self.notebook, True, True, 2)
  92. self.stop_button.set_sensitive(False)
  93. accelgroup = gtk.AccelGroup()
  94. self.add_accel_group(accelgroup)
  95. mod = gtklib.get_thg_modifier()
  96. key, modifier = gtk.accelerator_parse(mod+'w')
  97. notebook.add_accelerator('thg-close', accelgroup, key,
  98. modifier, gtk.ACCEL_VISIBLE)
  99. notebook.connect('thg-close', self.close_notebook)
  100. key, modifier = gtk.accelerator_parse(mod+'n')
  101. notebook.add_accelerator('thg-new', accelgroup, key,
  102. modifier, gtk.ACCEL_VISIBLE)
  103. notebook.connect('thg-new', self.new_notebook)
  104. # status bar
  105. hbox = gtk.HBox()
  106. style = csinfo.labelstyle(contents=('%(shortuser)s@%(revnum)s '
  107. '%(dateage)s', ' "%(summary)s"',), selectable=True)
  108. self.cslabel = csinfo.create(self.repo, style=style)
  109. hbox.pack_start(self.cslabel, False, False, 4)
  110. self.stbar = statusbar.StatusBar()
  111. hbox.pack_start(self.stbar)
  112. vbox.pack_start(hbox, False, False)
  113. return vbox
  114. def _destroying(self, gtkobj):
  115. self.stop_all_searches()
  116. gdialog.GWindow._destroying(self, gtkobj)
  117. def ann_header_context_menu(self, treeview):
  118. m = gtklib.MenuBuilder()
  119. m.append(_('Filename'), self.toggle_annatate_columns,
  120. ascheck=True, args=[treeview, 2])
  121. m.append(_('User'), self.toggle_annatate_columns,
  122. ascheck=True, args=[treeview, 3])
  123. menu = m.build()
  124. menu.show_all()
  125. return menu
  126. def grep_context_menu(self):
  127. m = gtklib.MenuBuilder()
  128. m.append(_('Di_splay Change'), self.cmenu_display,
  129. 'menushowchanged.ico')
  130. m.append(_('_Annotate File'), self.cmenu_annotate, 'menublame.ico')
  131. m.append(_('_File History'), self.cmenu_file_log, 'menulog.ico')
  132. m.append(_('_View File at Revision'), self.cmenu_view, gtk.STOCK_EDIT)
  133. menu = m.build()
  134. menu.show_all()
  135. return menu
  136. def annotate_context_menu(self, objs):
  137. m = gtklib.MenuBuilder()
  138. m.append(_('_Zoom to Change'), self.cmenu_zoom, gtk.STOCK_ZOOM_IN,
  139. args=[objs])
  140. m.append(_('Di_splay Change'), self.cmenu_display,
  141. 'menushowchanged.ico')
  142. (frame, treeview, filepath, graphview) = objs
  143. path = graphview.get_path_at_revid(int(self.currev))
  144. filepath = graphview.get_wfile_at_path(path)
  145. ctx = self.repo[self.currev]
  146. fctx = ctx.filectx(filepath)
  147. parents = fctx.parents()
  148. if len(parents) > 0:
  149. if len(parents) == 1:
  150. m.append(_('_Annotate Parent'), self.cmenu_annotate_1st_parent,
  151. 'menublame.ico', args=[objs])
  152. else:
  153. m.append(_('_Annotate First Parent'), self.cmenu_annotate_1st_parent,
  154. 'menublame.ico', args=[objs])
  155. m.append(_('Annotate Second Parent'), self.cmenu_annotate_2nd_parent,
  156. 'menublame.ico', args=[objs])
  157. m.append(_('_View File at Revision'), self.cmenu_view, gtk.STOCK_EDIT)
  158. m.append(_('_File History'), self.cmenu_file_log, 'menulog.ico')
  159. m.append(_('_Diff to Local'), self.cmenu_local_diff)
  160. menu = m.build()
  161. menu.show_all()
  162. return menu
  163. def cmenu_zoom(self, menuitem, objs):
  164. (frame, treeview, path, graphview) = objs
  165. graphview.scroll_to_revision(int(self.currev))
  166. graphview.set_revision_id(int(self.currev))
  167. def cmenu_display(self, menuitem):
  168. statopts = {'rev' : [self.currev] }
  169. dlg = changeset.ChangeSet(self.ui, self.repo, self.cwd, [], statopts)
  170. dlg.display()
  171. def cmenu_view(self, menuitem):
  172. self._node2 = self.currev
  173. self._view_files([self.curpath], False)
  174. def cmenu_annotate(self, menuitem):
  175. self.add_annotate_page(self.curpath, self.currev)
  176. def cmenu_annotate_1st_parent(self, menuitem, objs):
  177. self.annotate_parent(objs, 0)
  178. def cmenu_annotate_2nd_parent(self, menuitem, objs):
  179. self.annotate_parent(objs, 1)
  180. def annotate_parent(self, objs, parent_idx):
  181. (frame, treeview, filepath, graphview) = objs
  182. path = graphview.get_path_at_revid(int(self.currev))
  183. filepath = graphview.get_wfile_at_path(path)
  184. ctx = self.repo[self.currev]
  185. fctx = ctx.filectx(filepath)
  186. parents = fctx.parents()
  187. parent_fctx = parents[parent_idx]
  188. parent_revid = parent_fctx.changectx().rev()
  189. filepath = parent_fctx.path()
  190. # annotate file of parent rev
  191. self.trigger_annotate(parent_revid, filepath, objs)
  192. graphview.scroll_to_revision(int(parent_revid))
  193. graphview.set_revision_id(int(parent_revid))
  194. def cmenu_local_diff(self, menuitem):
  195. opts = {'rev':[str(self.currev)], 'bundle':None}
  196. self._do_diff([self.curpath], opts)
  197. def cmenu_file_log(self, menuitem):
  198. from tortoisehg.hgtk import history
  199. dlg = history.run(self.ui, filehist=self.curpath)
  200. dlg.display()
  201. def grep_button_release(self, widget, event):
  202. if event.button == 3 and not (event.state & (gtk.gdk.SHIFT_MASK |
  203. gtk.gdk.CONTROL_MASK)):
  204. self.grep_popup_menu(widget, event.button, event.time)
  205. return False
  206. def grep_popup_menu(self, treeview, button=0, time=0):
  207. self.grep_cmenu.popup(None, None, None, button, time)
  208. return True
  209. def grep_thgdiff(self, treeview):
  210. if self.currev:
  211. self._do_diff([], {'change' : self.currev})
  212. def grep_row_act(self, tree, path, column):
  213. 'Default action is the first entry in the context menu'
  214. self.grep_cmenu.get_children()[0].activate()
  215. return True
  216. def get_rev_desc(self, rev):
  217. if rev in self.changedesc:
  218. return self.changedesc[rev]
  219. ctx = self.repo[rev]
  220. author = hglib.toutf(hglib.username(ctx.user()))
  221. date = hglib.toutf(hglib.displaytime(ctx.date()))
  222. text = hglib.tounicode(ctx.description()).replace(u'\0', '')
  223. lines = text.splitlines()
  224. summary = hglib.toutf(lines and lines[0] or '')
  225. desc = gtklib.markup_escape_text('%s@%s %s "%s"' % \
  226. (author, rev, date, summary))
  227. self.changedesc[rev] = (desc, author)
  228. return self.changedesc[rev]
  229. def search_clicked(self, button, data):
  230. self.add_search_page()
  231. def create_tab_close_button(self):
  232. button = gtk.Button()
  233. iconBox = gtk.HBox(False, 0)
  234. image = gtk.Image()
  235. image.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
  236. gtk.Button.set_relief(button, gtk.RELIEF_NONE)
  237. settings = gtk.Widget.get_settings(button)
  238. (w,h) = gtk.icon_size_lookup_for_settings(settings, gtk.ICON_SIZE_MENU)
  239. gtk.Widget.set_size_request(button, w + 4, h + 4)
  240. image.show()
  241. iconBox.pack_start(image, True, False, 0)
  242. button.add(iconBox)
  243. iconBox.show()
  244. return button
  245. def close_notebook(self, notebook):
  246. if notebook.get_n_pages() <= 1:
  247. gtklib.thgclose(self)
  248. else:
  249. self.close_current_page()
  250. def new_notebook(self, notebook):
  251. self.add_search_page()
  252. def add_search_page(self):
  253. frame = gtk.Frame()
  254. frame.set_border_width(10)
  255. vbox = gtk.VBox()
  256. search_hbox = gtk.HBox()
  257. regexp = gtk.Entry()
  258. includes = gtk.Entry()
  259. if self.cwd.startswith(self.repo.root):
  260. try:
  261. relpath = util.canonpath(self.repo.root, self.cwd, '.')
  262. includes.set_text(relpath)
  263. except util.Abort:
  264. # Some paths inside root are invalid (.hg/*)
  265. pass
  266. excludes = gtk.Entry()
  267. search = gtk.Button(_('Search'))
  268. search_hbox.pack_start(gtk.Label(_('Regexp:')), False, False, 4)
  269. search_hbox.pack_start(regexp, True, True, 4)
  270. search_hbox.pack_start(gtk.Label(_('Includes:')), False, False, 4)
  271. search_hbox.pack_start(includes, True, True, 4)
  272. search_hbox.pack_start(gtk.Label(_('Excludes:')), False, False, 4)
  273. search_hbox.pack_start(excludes, True, True, 4)
  274. search_hbox.pack_start(search, False, False, 4)
  275. self.tooltips.set_tip(search, _('Start this search'))
  276. self.tooltips.set_tip(regexp, _('Regular expression search pattern'))
  277. self.tooltips.set_tip(includes, _('Comma separated list of'
  278. ' inclusion patterns. By default, the entire repository'
  279. ' is searched.'))
  280. self.tooltips.set_tip(excludes, _('Comma separated list of'
  281. ' exclusion patterns. Exclusion patterns are applied'
  282. ' after inclusion patterns.'))
  283. vbox.pack_start(search_hbox, False, False, 4)
  284. hbox = gtk.HBox()
  285. follow = gtk.CheckButton(_('Follow copies and renames'))
  286. ignorecase = gtk.CheckButton(_('Ignore case'))
  287. linenum = gtk.CheckButton(_('Show line numbers'))
  288. showall = gtk.CheckButton(_('Show all matching revisions'))
  289. hbox.pack_start(follow, False, False, 4)
  290. hbox.pack_start(ignorecase, False, False, 4)
  291. hbox.pack_start(linenum, False, False, 4)
  292. hbox.pack_start(showall, False, False, 4)
  293. vbox.pack_start(hbox, False, False, 4)
  294. treeview = gtk.TreeView()
  295. treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
  296. treeview.set_rules_hint(True)
  297. treeview.set_property('fixed-height-mode', True)
  298. treeview.connect("cursor-changed", self.grep_selection_changed)
  299. treeview.connect('button-release-event', self.grep_button_release)
  300. treeview.connect('popup-menu', self.grep_popup_menu)
  301. treeview.connect('row-activated', self.grep_row_act)
  302. accelgroup = gtk.AccelGroup()
  303. self.add_accel_group(accelgroup)
  304. mod = gtklib.get_thg_modifier()
  305. key, modifier = gtk.accelerator_parse(mod+'d')
  306. treeview.add_accelerator('thg-diff', accelgroup, key,
  307. modifier, gtk.ACCEL_VISIBLE)
  308. treeview.connect('thg-diff', self.grep_thgdiff)
  309. results = gtk.ListStore(str, # revision id
  310. str, # matched line (utf-8)
  311. str, # description (utf-8, escaped)
  312. str) # file path (utf-8)
  313. treeview.set_model(results)
  314. treeview.set_search_equal_func(self.search_in_grep)
  315. for title, width, ttype, col, emode in (
  316. (_('Rev'), 10, 'text', GCOL_REVID, pango.ELLIPSIZE_NONE),
  317. (_('File'), 25, 'text', GCOL_PATH, pango.ELLIPSIZE_START),
  318. (_('Matches'), 80, 'markup', GCOL_LINE, pango.ELLIPSIZE_END)):
  319. cell = gtk.CellRendererText()
  320. cell.set_property('width-chars', width)
  321. cell.set_property('ellipsize', emode)
  322. cell.set_property('family', 'Monospace')
  323. column = gtk.TreeViewColumn(title)
  324. column.set_resizable(True)
  325. column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  326. column.set_fixed_width(cell.get_size(treeview)[2])
  327. column.pack_start(cell, expand=True)
  328. column.add_attribute(cell, ttype, col)
  329. treeview.append_column(column)
  330. if hasattr(treeview, 'set_tooltip_column'):
  331. treeview.set_tooltip_column(GCOL_DESC)
  332. scroller = gtk.ScrolledWindow()
  333. scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  334. scroller.add(treeview)
  335. vbox.pack_start(scroller, True, True)
  336. frame.add(vbox)
  337. frame.show_all()
  338. hbox = gtk.HBox()
  339. lbl = gtk.Label(_('Search %d') % self.newpagecount)
  340. close = self.create_tab_close_button()
  341. close.connect('clicked', self.close_page, frame)
  342. hbox.pack_start(lbl, True, True, 2)
  343. hbox.pack_start(close, False, False)
  344. hbox.show_all()
  345. num = self.notebook.append_page(frame, hbox)
  346. self.newpagecount += 1
  347. objs = (treeview.get_model(), frame, regexp, follow, ignorecase,
  348. excludes, includes, linenum, showall, search_hbox)
  349. # Clicking 'search' or hitting Enter in any text entry triggers search
  350. search.connect('clicked', self.trigger_search, objs)
  351. regexp.connect('activate', self.trigger_search, objs)
  352. includes.connect('activate', self.trigger_search, objs)
  353. excludes.connect('activate', self.trigger_search, objs)
  354. # Includes/excludes must disable following copies
  355. objs = (includes, excludes, follow)
  356. includes.connect('changed', self.update_following_possible, objs)
  357. excludes.connect('changed', self.update_following_possible, objs)
  358. self.update_following_possible(includes, objs)
  359. if hasattr(self.notebook, 'set_tab_reorderable'):
  360. self.notebook.set_tab_reorderable(frame, True)
  361. self.notebook.set_current_page(num)
  362. regexp.grab_focus()
  363. def search_in_grep(self, model, column, key, iter):
  364. """Searches all fields shown in the tree when the user hits crtr+f,
  365. not just the ones that are set via tree.set_search_column.
  366. Case insensitive
  367. """
  368. key = key.lower()
  369. for col in (GCOL_PATH, GCOL_LINE):
  370. if key in model.get_value(iter, col).lower():
  371. return False
  372. return True
  373. def trigger_search(self, button, objs):
  374. (model, frame, regexp, follow, ignorecase,
  375. excludes, includes, linenum, showall, search_hbox) = objs
  376. retext = regexp.get_text()
  377. if not retext:
  378. gdialog.Prompt(_('No regular expression given'),
  379. _('You must provide a search expression'), self).run()
  380. regexp.grab_focus()
  381. return
  382. try:
  383. re.compile(retext)
  384. except re.error, e:
  385. gdialog.Prompt(_('Invalid regular expression'),
  386. _('Error: %s') % str(e), self).run()
  387. regexp.grab_focus()
  388. return
  389. q = Queue.Queue()
  390. args = [q, 'grep']
  391. if follow.get_active(): args.append('--follow')
  392. if ignorecase.get_active(): args.append('--ignore-case')
  393. if linenum.get_active(): args.append('--line-number')
  394. if showall.get_active(): args.append('--all')
  395. incs = [x.strip() for x in includes.get_text().split(',')]
  396. excs = [x.strip() for x in excludes.get_text().split(',')]
  397. for i in incs:
  398. if i: args.extend(['-I', i])
  399. for x in excs:
  400. if x: args.extend(['-X', x])
  401. args.append(retext)
  402. def threadfunc(q, *args):
  403. try:
  404. hglib.hgcmd_toq(q, True, args)
  405. except (util.Abort, error.LookupError), e:
  406. self.stbar.set_text(_('Abort: %s') % str(e))
  407. thread = thread2.Thread(target=threadfunc, args=args)
  408. thread.start()
  409. frame._mythread = thread
  410. self.stop_button.set_sensitive(True)
  411. model.clear()
  412. search_hbox.set_sensitive(False)
  413. self.stbar.begin(msg='hg ' + ' '.join(args[2:]))
  414. hbox = gtk.HBox()
  415. lbl = gtk.Label(_('Search "%s"') % retext.split()[0])
  416. close = self.create_tab_close_button()
  417. close.connect('clicked', self.close_page, frame)
  418. hbox.pack_start(lbl, True, True, 2)
  419. hbox.pack_start(close, False, False)
  420. hbox.show_all()
  421. self.notebook.set_tab_label(frame, hbox)
  422. gobject.timeout_add(50, self.grep_wait, thread, q, model,
  423. search_hbox, regexp, frame)
  424. def grep_wait(self, thread, q, model, search_hbox, regexp, frame):
  425. """
  426. Handle all the messages currently in the queue (if any).
  427. """
  428. text = ''
  429. while q.qsize():
  430. data, label = q.get(0)
  431. data = gtklib.markup_escape_text(hglib.toutf(data))
  432. if label == 'grep.match':
  433. text += '<span foreground="red"><b>%s</b></span>' % data
  434. else:
  435. text += data
  436. for line in text.splitlines():
  437. try:
  438. (path, revid, text) = line.split(':', 2)
  439. desc, user = self.get_rev_desc(long(revid))
  440. except ValueError:
  441. continue
  442. if self.tabwidth:
  443. text = text.expandtabs(self.tabwidth)
  444. model.append((revid, text, desc, hglib.toutf(path)))
  445. if thread.isAlive():
  446. return True
  447. else:
  448. if threading.activeCount() == 1:
  449. self.stop_button.set_sensitive(False)
  450. frame._mythread = None
  451. search_hbox.set_sensitive(True)
  452. regexp.grab_focus()
  453. self.stbar.end()
  454. return False
  455. def grep_selection_changed(self, treeview):
  456. """
  457. Callback for when the user selects grep output.
  458. """
  459. (path, focus) = treeview.get_cursor()
  460. model = treeview.get_model()
  461. if path is not None and model is not None:
  462. iter = model.get_iter(path)
  463. self.currev = model[iter][GCOL_REVID]
  464. self.curpath = hglib.fromutf(model[iter][GCOL_PATH])
  465. self.cslabel.update(model[iter][GCOL_REVID])
  466. def close_current_page(self):
  467. num = self.notebook.get_current_page()
  468. if num != -1 and self.notebook.get_n_pages():
  469. self.notebook.remove_page(num)
  470. def stop_current_search(self, button, widget):
  471. num = self.notebook.get_current_page()
  472. frame = self.notebook.get_nth_page(num)
  473. self.stop_search(frame)
  474. def stop_all_searches(self):
  475. for num in xrange(self.notebook.get_n_pages()):
  476. frame = self.notebook.get_nth_page(num)
  477. self.stop_search(frame)
  478. def stop_search(self, frame):
  479. if getattr(frame, '_mythread', None):
  480. if frame._mythread.isAlive():
  481. try:
  482. frame._mythread.terminate()
  483. frame._mythread.join()
  484. except (threading.ThreadError, ValueError):
  485. pass
  486. frame._mythread = None
  487. def close_page(self, button, widget):
  488. '''Close page button has been pressed'''
  489. num = self.notebook.page_num(widget)
  490. if num != -1:
  491. self.notebook.remove_page(num)
  492. if self.notebook.get_n_pages() < 1:
  493. self.newpagecount = 1
  494. self.add_search_page()
  495. def add_header_context_menu(self, col, menu):
  496. lb = gtk.Label(col.get_title())
  497. lb.show()
  498. col.set_widget(lb)
  499. wgt = lb.get_parent()
  500. while wgt:
  501. if type(wgt) == gtk.Button:
  502. wgt.connect("button-press-event",
  503. self.tree_header_button_press, menu)
  504. break
  505. wgt = wgt.get_parent()
  506. def tree_header_button_press(self, widget, event, menu):
  507. if event.button == 3:
  508. menu.popup(None, None, None, event.button, event.time)
  509. return True
  510. return False
  511. def update_following_possible(self, widget, objs):
  512. (includes, excludes, follow) = objs
  513. allow = not includes.get_text() and not excludes.get_text()
  514. if not allow:
  515. follow.set_active(False)
  516. follow.set_sensitive(allow)
  517. def add_annotate_page(self, path, revid):
  518. '''
  519. Add new annotation page to notebook. Start scan of
  520. file 'path' revision history, start annotate of supplied
  521. revision 'revid'.
  522. '''
  523. if revid == '.':
  524. ctx = self.repo.parents()[0]
  525. try:
  526. fctx = ctx.filectx(path)
  527. except error.LookupError:
  528. gdialog.Prompt(_('File is unrevisioned'),
  529. _('Unable to annotate ') + path, self).run()
  530. return
  531. rev = fctx.filelog().linkrev(fctx.filerev())
  532. revid = str(rev)
  533. else:
  534. rev = long(revid)
  535. frame = gtk.Frame()
  536. frame.set_border_width(10)
  537. vbox = gtk.VBox()
  538. graphopts = { 'date': None, 'no_merges':False, 'only_merges':False,
  539. 'keyword':[], 'branch':None, 'pats':[], 'revrange':[],
  540. 'revlist':[], 'noheads':False, 'orig-tip':len(self.repo),
  541. 'branch-view':False, 'rev':[], 'npreviews':0 }
  542. graphopts['filehist'] = path
  543. # File log revision graph
  544. graphview = LogTreeView(self.repo, 5000)
  545. graphview.set_property('rev-column-visible', True)
  546. graphview.set_property('msg-column-visible', True)
  547. graphview.set_property('user-column-visible', True)
  548. graphview.set_property('age-column-visible', True)
  549. graphview.set_columns(['graph', 'rev', 'msg', 'user', 'age'])
  550. graphview.connect('revisions-loaded', self.revisions_loaded, rev)
  551. graphview.refresh(True, [path], graphopts)
  552. # Annotation text tree view
  553. treeview = gtk.TreeView()
  554. treeview.get_selection().set_mode(gtk.SELECTION_SINGLE)
  555. treeview.set_property('fixed-height-mode', True)
  556. treeview.set_border_width(0)
  557. accelgroup = gtk.AccelGroup()
  558. self.add_accel_group(accelgroup)
  559. mod = gtklib.get_thg_modifier()
  560. key, modifier = gtk.accelerator_parse(mod+'d')
  561. treeview.add_accelerator('thg-diff', accelgroup, key,
  562. modifier, gtk.ACCEL_VISIBLE)
  563. treeview.connect('thg-diff', self.annotate_thgdiff)
  564. results = gtk.ListStore(str, # revision id
  565. str, # file line (utf-8)
  566. str, # description (utf-8, escaped)
  567. str, # file path (utf-8)
  568. str, # color
  569. str, # author (utf-8)
  570. str) # line number
  571. treeview.set_model(results)
  572. treeview.set_search_equal_func(self.search_in_file)
  573. context_menu = self.ann_header_context_menu(treeview)
  574. for title, width, col, emode, visible in (
  575. (_('Line'), 8, ACOL_LNUM, pango.ELLIPSIZE_NONE, True),
  576. (_('Rev'), 10, ACOL_REVID, pango.ELLIPSIZE_NONE, True),
  577. (_('File'), 15, ACOL_PATH, pango.ELLIPSIZE_START, False),
  578. (_('User'), 15, ACOL_USER, pango.ELLIPSIZE_END, False),
  579. (_('Source'), 80, ACOL_LINE, pango.ELLIPSIZE_END, True)):
  580. cell = gtk.CellRendererText()
  581. cell.set_property('width-chars', width)
  582. cell.set_property('ellipsize', emode)
  583. cell.set_property('family', 'Monospace')
  584. column = gtk.TreeViewColumn(title)
  585. column.set_resizable(True)
  586. column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
  587. column.set_fixed_width(cell.get_size(treeview)[2])
  588. column.pack_start(cell, expand=True)
  589. column.add_attribute(cell, 'text', col)
  590. column.add_attribute(cell, 'background', ACOL_COLOR)
  591. column.set_visible(visible)
  592. treeview.append_column(column)
  593. self.add_header_context_menu(column, context_menu)
  594. treeview.set_headers_clickable(True)
  595. if hasattr(treeview, 'set_tooltip_column'):
  596. treeview.set_tooltip_column(ACOL_DESC)
  597. results.path = path
  598. results.rev = revid
  599. scroller = gtk.ScrolledWindow()
  600. scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  601. scroller.add(treeview)
  602. vpaned = gtk.VPaned()
  603. vpaned.pack1(graphview, True, True)
  604. vpaned.pack2(scroller, True, True)
  605. vbox.pack_start(vpaned, True, True)
  606. frame.add(vbox)
  607. frame.show_all()
  608. hbox = gtk.HBox()
  609. lbl = gtk.Label(hglib.toutf(os.path.basename(path) + '@' + revid))
  610. close = self.create_tab_close_button()
  611. close.connect('clicked', self.close_page, frame)
  612. hbox.pack_start(lbl, True, True, 2)
  613. hbox.pack_start(close, False, False)
  614. hbox.show_all()
  615. num = self.notebook.append_page_menu(frame,
  616. hbox, gtk.Label(hglib.toutf(path + '@' + revid)))
  617. if hasattr(self.notebook, 'set_tab_reorderable'):
  618. self.notebook.set_tab_reorderable(frame, True)
  619. self.notebook.set_current_page(num)
  620. graphview.connect('revision-selected', self.log_selection_changed, path)
  621. objs = (frame, treeview, path, graphview)
  622. graphview.treeview.connect('row-activated', self.log_activate, objs)
  623. graphview.treeview.connect('button-release-event',
  624. self.ann_button_release, objs)
  625. graphview.treeview.connect('popup-menu', self.ann_popup_menu, objs)
  626. treeview.connect("cursor-changed", self.ann_selection_changed)
  627. treeview.connect('button-release-event', self.ann_button_release, objs)
  628. treeview.connect('popup-menu', self.ann_popup_menu, objs)
  629. treeview.connect('row-activated', self.ann_row_act, objs)
  630. self.stbar.begin(msg=_('Loading history...'))
  631. def search_in_file(self, model, column, key, iter):
  632. """Searches all fields shown in the tree when the user hits crtr+f,
  633. not just the ones that are set via tree.set_search_column.
  634. Case insensitive
  635. """
  636. key = key.lower()
  637. for col in (ACOL_USER, ACOL_LINE):
  638. if key in model.get_value(iter, col).lower():
  639. return False
  640. return True
  641. def annotate_thgdiff(self, treeview):
  642. self._do_diff([], {'change' : self.currev})
  643. def toggle_annatate_columns(self, button, treeview, col):
  644. b = button.get_active()
  645. treeview.get_column(col).set_visible(b)
  646. def log_selection_changed(self, graphview, path):
  647. treeview = graphview.treeview
  648. (model, paths) = treeview.get_selection().get_selected_rows()
  649. if not paths:
  650. return
  651. revid = graphview.get_revid_at_path(paths[0])
  652. self.currev = str(revid)
  653. wfile = graphview.get_wfile_at_path(paths[0])
  654. if wfile:
  655. self.curpath = wfile
  656. def log_activate(self, treeview, path, column, objs):
  657. (frame, treeview, file, graphview) = objs
  658. rev = graphview.get_revid_at_path(path)
  659. wfile = graphview.get_wfile_at_path(path)
  660. self.trigger_annotate(rev, wfile, objs)
  661. def revisions_loaded(self, graphview, rev):
  662. self.stbar.end()
  663. graphview.set_revision_id(rev)
  664. treeview = graphview.treeview
  665. path, column = treeview.get_cursor()
  666. # It's possible that the requested change was not found in the
  667. # file's filelog history. In that case, no row will be
  668. # selected.
  669. if path != None and column != None:
  670. treeview.row_activated(path, column)
  671. def trigger_annotate(self, rev, path, objs):
  672. '''
  673. User has selected a file revision to annotate. Trigger a
  674. background thread to perform the annotation. Disable the select
  675. button until this operation is complete.
  676. '''
  677. def threadfunc(q, *args):
  678. try:
  679. hglib.hgcmd_toq(q, False, args)
  680. except (util.Abort, error.LookupError), e:
  681. self.stbar.set_text(_('Abort: %s') % str(e))
  682. (frame, treeview, origpath, graphview) = objs
  683. q = Queue.Queue()
  684. # Use short -f here because it's meaning has changed, it used
  685. # to be --follow but now it means --file. We want either.
  686. # Replace -f with --file when support for hg-1.4 is dropped
  687. args = [q, 'annotate', '-f', '--number', '--rev', str(rev),
  688. 'path:'+path]
  689. thread = thread2.Thread(target=threadfunc, args=args)
  690. thread.start()
  691. frame._mythread = thread
  692. self.stop_button.set_sensitive(True)
  693. # date of selected revision
  694. ctx = self.repo[long(rev)]
  695. curdate = ctx.date()[0]
  696. colormap = AnnotateColorSaturation()
  697. model, rows = treeview.get_selection().get_selected_rows()
  698. model.clear()
  699. self.stbar.begin(msg=hglib.toutf('hg ' + ' '.join(args[2:])))
  700. hbox = gtk.HBox()
  701. lbl = gtk.Label(hglib.toutf(os.path.basename(path) + '@' + str(rev)))
  702. close = self.create_tab_close_button()
  703. close.connect('clicked', self.close_page, frame)
  704. hbox.pack_start(lbl, True, True, 2)
  705. hbox.pack_start(close, False, False)
  706. hbox.show_all()
  707. self.notebook.set_tab_label(frame, hbox)
  708. gobject.timeout_add(50, self.annotate_wait, thread, q, treeview,
  709. curdate, colormap, frame, rows)
  710. def annotate_wait(self, thread, q, tview, curdate, colormap, frame, rows):
  711. """
  712. Handle all the messages currently in the queue (if any).
  713. """
  714. model = tview.get_model()
  715. while q.qsize():
  716. line = q.get(0).rstrip('\r\n')
  717. try:
  718. (revpath, text) = line.split(':', 1)
  719. revid, path = revpath.lstrip().split(' ', 1)
  720. rowrev = long(revid)
  721. except ValueError:
  722. continue
  723. desc, user = self.get_rev_desc(rowrev)
  724. ctx = self.repo[rowrev]
  725. color = colormap.get_color(ctx, curdate)
  726. if self.tabwidth:
  727. text = text.expandtabs(self.tabwidth)
  728. model.append((revid, hglib.toutf(text[:512]), desc,
  729. hglib.toutf(path.strip()), color, user, len(model)+1))
  730. if thread.isAlive():
  731. return True
  732. else:
  733. if threading.activeCount() == 1:
  734. self.stop_button.set_sensitive(False)
  735. if rows:
  736. tview.get_selection().select_path(rows[0])
  737. tview.scroll_to_cell(rows[0], use_align=True, row_align=0.5)
  738. tview.grab_focus()
  739. frame._mythread = None
  740. self.stbar.end()
  741. return False
  742. def ann_selection_changed(self, treeview):
  743. """
  744. User selected line of annotate output, describe revision
  745. responsible for this line in the status bar
  746. """
  747. (path, focus) = treeview.get_cursor()
  748. model = treeview.get_model()
  749. if path is not None and model is not None:
  750. anniter = model.get_iter(path)
  751. self.currev = model[anniter][ACOL_REVID]
  752. self.path = model.path
  753. self.cslabel.update(model[anniter][ACOL_REVID])
  754. def ann_button_release(self, widget, event, objs):
  755. if event.button == 3 and not (event.state & (gtk.gdk.SHIFT_MASK |
  756. gtk.gdk.CONTROL_MASK)):
  757. self.ann_popup_menu(widget, event.button, event.time, objs)
  758. return False
  759. def ann_popup_menu(self, treeview, button, time, objs):
  760. ann_cmenu = self.annotate_context_menu(objs)
  761. ann_cmenu.popup(None, None, None, button, time)
  762. return True
  763. def ann_row_act(self, tree, path, column, objs):
  764. ann_cmenu = self.annotate_context_menu(objs)
  765. ann_cmenu.get_children()[0].activate()
  766. def run(ui, *pats, **opts):
  767. cmdoptions = {
  768. 'follow':False, 'follow-first':False, 'copies':False, 'keyword':[],
  769. 'limit':0, 'rev':[], 'removed':False, 'no_merges':False, 'date':None,
  770. 'only_merges':None, 'prune':[], 'git':False, 'verbose':False,
  771. 'include':[], 'exclude':[]
  772. }
  773. return DataMineDialog(ui, None, None, pats, cmdoptions)