/tortoisehg/hgtk/guess.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 387 lines · 302 code · 49 blank · 36 comment · 47 complexity · a1fe76ca69c455b821a639a70f7a1da2 MD5 · raw file

  1. # guess.py - TortoiseHg's dialogs for detecting copies and renames
  2. #
  3. # Copyright 2009 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 os
  8. import sys
  9. import gtk
  10. import gobject
  11. import pango
  12. import cStringIO
  13. import Queue
  14. from mercurial import hg, ui, mdiff, cmdutil, match, util, error
  15. from mercurial import similar
  16. from tortoisehg.util.i18n import _
  17. from tortoisehg.util import hglib, shlib, paths, thread2, settings
  18. from tortoisehg.hgtk import gtklib, statusbar
  19. class DetectRenameDialog(gtk.Window):
  20. 'Detect renames after they occur'
  21. def __init__(self):
  22. 'Initialize the Dialog'
  23. gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
  24. gtklib.set_tortoise_icon(self, 'detect_rename.ico')
  25. gtklib.set_tortoise_keys(self)
  26. try:
  27. repo = hg.repository(ui.ui(), path=paths.find_root())
  28. except error.RepoError:
  29. gtklib.idle_add_single_call(self.destroy)
  30. return
  31. self.repo = repo
  32. self.notify_func = None
  33. reponame = hglib.get_reponame(repo)
  34. self.set_title(_('Detect Copies/Renames in %s') % reponame)
  35. self.settings = settings.Settings('guess')
  36. dims = self.settings.get_value('dims', (800, 600))
  37. self.set_default_size(dims[0], dims[1])
  38. # vbox for dialog main & status bar
  39. mainvbox = gtk.VBox()
  40. self.add(mainvbox)
  41. # vsplit for top & diff
  42. self.vpaned = gtk.VPaned()
  43. mainvbox.pack_start(self.vpaned, True, True, 2)
  44. pos = self.settings.get_value('vpaned', None)
  45. if pos: self.vpaned.set_position(pos)
  46. # vbox for top contents
  47. topvbox = gtk.VBox()
  48. self.vpaned.pack1(topvbox, True, False)
  49. # frame for simularity
  50. frame = gtk.Frame(_('Minimum Simularity Percentage'))
  51. topvbox.pack_start(frame, False, False, 2)
  52. #$ simularity slider
  53. self.adjustment = gtk.Adjustment(50, 0, 100, 1)
  54. value = self.settings.get_value('percent', None)
  55. if value: self.adjustment.set_value(value)
  56. hscale = gtk.HScale(self.adjustment)
  57. frame.add(hscale)
  58. # horizontal splitter for unknown & candidate
  59. self.hpaned = gtk.HPaned()
  60. topvbox.pack_start(self.hpaned, True, True, 2)
  61. pos = self.settings.get_value('hpaned', None)
  62. if pos: self.hpaned.set_position(pos)
  63. #$ frame for unknown list
  64. unknownframe = gtk.Frame(_('Unrevisioned Files'))
  65. self.hpaned.pack1(unknownframe, True, True)
  66. #$$ vbox for unknown list & rename/copy buttons
  67. unkvbox = gtk.VBox()
  68. unknownframe.add(unkvbox)
  69. #$$$ scroll window for unknown list
  70. scroller = gtk.ScrolledWindow()
  71. unkvbox.pack_start(scroller, True, True, 2)
  72. scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  73. #$$$$ unknown list
  74. unkmodel = gtk.ListStore(str, # path
  75. str) # path (utf-8)
  76. self.unktree = gtk.TreeView(unkmodel)
  77. scroller.add(self.unktree)
  78. self.unktree.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
  79. cell = gtk.CellRendererText()
  80. cell.set_property("ellipsize", pango.ELLIPSIZE_START)
  81. col = gtk.TreeViewColumn('File', cell, text=1)
  82. self.unktree.append_column(col)
  83. self.unktree.set_enable_search(True)
  84. self.unktree.set_headers_visible(False)
  85. #$$$ hbox for rename/copy buttons
  86. btnhbox = gtk.HBox()
  87. unkvbox.pack_start(btnhbox, False, False, 2)
  88. #$$$$ rename/copy buttons in unknown frame
  89. self.renamebtn = gtk.Button(_('Find Renames'))
  90. self.renamebtn.set_sensitive(False)
  91. btnhbox.pack_start(self.renamebtn, False, False, 2)
  92. self.copybtn = gtk.Button(_('Find Copies'))
  93. self.copybtn.set_sensitive(False)
  94. btnhbox.pack_start(self.copybtn, False, False, 2)
  95. #$ frame for candidate list
  96. candidateframe = gtk.Frame(_('Candidate Matches'))
  97. self.hpaned.pack2(candidateframe, True, True)
  98. #$$ vbox for candidate list & accept button
  99. canvbox = gtk.VBox()
  100. candidateframe.add(canvbox)
  101. #$$$ scroll window for candidate list
  102. scroller = gtk.ScrolledWindow()
  103. canvbox.pack_start(scroller, True, True, 2)
  104. scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  105. #$$$$ candidate list
  106. canmodel = gtk.ListStore(str, # source
  107. str, # source (utf-8)
  108. str, # dest
  109. str, # dest (utf-8)
  110. str, # percent
  111. bool) # sensitive
  112. self.cantree = gtk.TreeView(canmodel)
  113. scroller.add(self.cantree)
  114. self.cantree.set_rules_hint(True)
  115. self.cantree.set_reorderable(False)
  116. self.cantree.set_enable_search(False)
  117. self.cantree.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
  118. cell = gtk.CellRendererText()
  119. cell.set_property('width-chars', 30)
  120. cell.set_property('ellipsize', pango.ELLIPSIZE_START)
  121. col = gtk.TreeViewColumn(_('Source'), cell, text=1, sensitive=5)
  122. col.set_resizable(True)
  123. self.cantree.append_column(col)
  124. cell = gtk.CellRendererText()
  125. cell.set_property('width-chars', 30)
  126. cell.set_property('ellipsize', pango.ELLIPSIZE_START)
  127. col = gtk.TreeViewColumn(_('Dest'), cell, text=3, sensitive=5)
  128. col.set_resizable(True)
  129. self.cantree.append_column(col)
  130. cell = gtk.CellRendererText()
  131. cell.set_property('width-chars', 5)
  132. cell.set_property('ellipsize', pango.ELLIPSIZE_NONE)
  133. col = gtk.TreeViewColumn('%', cell, text=4, sensitive=5)
  134. col.set_resizable(True)
  135. self.cantree.append_column(col)
  136. #$$$ hbox for accept button
  137. btnhbox = gtk.HBox()
  138. canvbox.pack_start(btnhbox, False, False, 2)
  139. #$$$$ accept button in candidate frame
  140. self.acceptbtn = gtk.Button(_('Accept Match'))
  141. btnhbox.pack_start(self.acceptbtn, False, False, 2)
  142. self.acceptbtn.set_sensitive(False)
  143. # frame for diff
  144. diffframe = gtk.Frame(_('Differences from Source to Dest'))
  145. self.vpaned.pack2(diffframe)
  146. diffframe.set_shadow_type(gtk.SHADOW_ETCHED_IN)
  147. #$ scroll window for diff
  148. scroller = gtk.ScrolledWindow()
  149. diffframe.add(scroller)
  150. scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
  151. #$$ text view for diff
  152. self.buf = gtk.TextBuffer()
  153. self.buf.create_tag('removed', foreground=gtklib.DRED)
  154. self.buf.create_tag('added', foreground=gtklib.DGREEN)
  155. self.buf.create_tag('position', foreground=gtklib.DORANGE)
  156. self.buf.create_tag('header', foreground=gtklib.DBLUE)
  157. diffview = gtk.TextView(self.buf)
  158. scroller.add(diffview)
  159. fontdiff = hglib.getfontconfig()['fontdiff']
  160. diffview.modify_font(pango.FontDescription(fontdiff))
  161. diffview.set_wrap_mode(gtk.WRAP_NONE)
  162. diffview.set_editable(False)
  163. # status bar
  164. self.stbar = statusbar.StatusBar()
  165. mainvbox.pack_start(self.stbar, False, False, 2)
  166. # register signal handlers
  167. self.copybtn.connect('pressed', lambda b: self.find_copies())
  168. self.renamebtn.connect('pressed', lambda b: self.find_renames())
  169. self.acceptbtn.connect('pressed', lambda b: self.accept_match())
  170. self.unktree.get_selection().connect('changed', self.unknown_sel_change)
  171. self.cantree.connect('row-activated', lambda t,p,c: self.accept_match())
  172. self.cantree.get_selection().connect('changed', self.show_diff)
  173. self.connect('delete-event', lambda *a: self.save_settings())
  174. gtklib.idle_add_single_call(self.refresh)
  175. def set_notify_func(self, func):
  176. self.notify_func = func
  177. def refresh(self):
  178. q = Queue.Queue()
  179. unkmodel = self.unktree.get_model()
  180. unkmodel.clear()
  181. thread = thread2.Thread(target=self.unknown_thread, args=(q,))
  182. thread.start()
  183. gobject.timeout_add(50, self.unknown_wait, thread, q)
  184. def unknown_thread(self, q):
  185. hglib.invalidaterepo(self.repo)
  186. matcher = match.always(self.repo.root, self.repo.root)
  187. status = self.repo.status(node1=self.repo.dirstate.parents()[0],
  188. node2=None, match=matcher, ignored=False,
  189. clean=False, unknown=True)
  190. (modified, added, removed, deleted, unknown, ignored, clean) = status
  191. for u in unknown:
  192. q.put( u )
  193. for a in added:
  194. if not self.repo.dirstate.copied(a):
  195. q.put( a )
  196. def unknown_wait(self, thread, q):
  197. unkmodel = self.unktree.get_model()
  198. while q.qsize():
  199. wfile = q.get(0)
  200. unkmodel.append( [wfile, hglib.toutf(wfile)] )
  201. return thread.isAlive()
  202. def save_settings(self):
  203. self.settings.set_value('vpaned', self.vpaned.get_position())
  204. self.settings.set_value('hpaned', self.hpaned.get_position())
  205. self.settings.set_value('percent', self.adjustment.get_value())
  206. rect = self.get_allocation()
  207. self.settings.set_value('dims', (rect.width, rect.height))
  208. self.settings.write()
  209. def find_renames(self, copy=False):
  210. 'User pressed "find renames" button'
  211. canmodel = self.cantree.get_model()
  212. canmodel.clear()
  213. umodel, upaths = self.unktree.get_selection().get_selected_rows()
  214. if not upaths:
  215. return
  216. tgts = [ umodel[p][0] for p in upaths ]
  217. q = Queue.Queue()
  218. thread = thread2.Thread(target=self.search_thread,
  219. args=(q, tgts, copy))
  220. thread.start()
  221. self.stbar.begin()
  222. text = _('finding source of ') + ', '.join(tgts)
  223. if len(text) > 60:
  224. text = text[:60]+'...'
  225. self.stbar.set_text(text)
  226. gobject.timeout_add(50, self.search_wait, thread, q)
  227. def search_thread(self, q, tgts, copy):
  228. hglib.invalidaterepo(self.repo)
  229. srcs = []
  230. audit_path = util.path_auditor(self.repo.root)
  231. m = cmdutil.match(self.repo)
  232. for abs in self.repo.walk(m):
  233. target = self.repo.wjoin(abs)
  234. good = True
  235. try:
  236. audit_path(abs)
  237. except:
  238. good = False
  239. status = self.repo.dirstate[abs]
  240. if (not good or not os.path.lexists(target)
  241. or (os.path.isdir(target) and not os.path.islink(target))):
  242. srcs.append(abs)
  243. elif copy and status == 'n':
  244. # looking for copies, so any revisioned file is a
  245. # potential source (yes, this will be expensive)
  246. # Added and removed files are not considered as copy
  247. # sources.
  248. srcs.append(abs)
  249. if copy:
  250. simularity = 1.0
  251. else:
  252. simularity = self.adjustment.get_value() / 100.0;
  253. gen = similar.findrenames(self.repo, tgts, srcs, simularity)
  254. for old, new, score in gen:
  255. q.put( [old, new, '%d%%' % (score*100)] )
  256. def search_wait(self, thread, q):
  257. canmodel = self.cantree.get_model()
  258. while canmodel and q.qsize():
  259. source, dest, sim = q.get(0)
  260. canmodel.append( [source, hglib.toutf(source), dest,
  261. hglib.toutf(dest), sim, True] )
  262. if thread.isAlive():
  263. return True
  264. else:
  265. self.stbar.end()
  266. return False
  267. def find_copies(self):
  268. 'User pressed "find copies" button'
  269. # call rename function with simularity = 100%
  270. self.find_renames(copy=True)
  271. def accept_match(self):
  272. 'User pressed "accept match" button'
  273. hglib.invalidaterepo(self.repo)
  274. wctx = self.repo[None]
  275. canmodel, upaths = self.cantree.get_selection().get_selected_rows()
  276. for path in upaths:
  277. row = canmodel[path]
  278. src, usrc, dest, udest, percent, sensitive = row
  279. if not sensitive:
  280. continue
  281. if not os.path.exists(self.repo.wjoin(src)):
  282. # Mark missing rename source as removed
  283. wctx.remove([src])
  284. wctx.copy(src, dest)
  285. shlib.shell_notify([self.repo.wjoin(src), self.repo.wjoin(dest)])
  286. if self.notify_func:
  287. self.notify_func()
  288. # Mark all rows with this target file as non-sensitive
  289. for row in canmodel:
  290. if row[2] == dest:
  291. row[5] = False
  292. self.refresh()
  293. def unknown_sel_change(self, selection):
  294. 'User selected a row in the unknown tree'
  295. model, upaths = selection.get_selected_rows()
  296. sensitive = upaths and True or False
  297. self.renamebtn.set_sensitive(sensitive)
  298. self.copybtn.set_sensitive(sensitive)
  299. def show_diff(self, selection):
  300. 'User selected a row in the candidate tree'
  301. hglib.invalidaterepo(self.repo)
  302. model, cpaths = selection.get_selected_rows()
  303. sensitive = cpaths and True or False
  304. self.acceptbtn.set_sensitive(sensitive)
  305. self.buf.set_text('')
  306. bufiter = self.buf.get_start_iter()
  307. for path in cpaths:
  308. row = model[path]
  309. src, usrc, dest, udest, percent, sensitive = row
  310. if not sensitive:
  311. continue
  312. ctx = self.repo['.']
  313. aa = self.repo.wread(dest)
  314. rr = ctx.filectx(src).data()
  315. opts = mdiff.defaultopts
  316. difftext = mdiff.unidiff(rr, '', aa, '', src, dest, None, opts=opts)
  317. if not difftext:
  318. l = _('== %s and %s have identical contents ==\n\n') % (src, dest)
  319. self.buf.insert(bufiter, l)
  320. continue
  321. difflines = difftext.splitlines(True)
  322. for line in difflines:
  323. line = hglib.toutf(line)
  324. if line.startswith('---') or line.startswith('+++'):
  325. self.buf.insert_with_tags_by_name(bufiter, line, 'header')
  326. elif line.startswith('-'):
  327. line = hglib.diffexpand(line)
  328. self.buf.insert_with_tags_by_name(bufiter, line, 'removed')
  329. elif line.startswith('+'):
  330. line = hglib.diffexpand(line)
  331. self.buf.insert_with_tags_by_name(bufiter, line, 'added')
  332. elif line.startswith('@@'):
  333. self.buf.insert_with_tags_by_name(bufiter, line, 'position')
  334. else:
  335. line = hglib.diffexpand(line)
  336. self.buf.insert(bufiter, line)
  337. def run(ui, *pats, **opts):
  338. return DetectRenameDialog()