PageRenderTime 57ms CodeModel.GetById 22ms app.highlight 30ms RepoModel.GetById 2ms app.codeStats 0ms

/tortoisehg/hgtk/guess.py

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