/b-gtk/fix-viz

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/b-gtk/fix-viz

« back to all changes in this revision

Viewing changes to diff.py

  • Committer: Aaron Bentley
  • Date: 2008-02-23 06:39:14 UTC
  • Revision ID: aaron@aaronbentley.com-20080223063914-49gdt7ed630rug9b
Add handle-patch script

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
1
# -*- coding: UTF-8 -*-
3
2
"""Difference window.
4
3
 
12
11
 
13
12
from cStringIO import StringIO
14
13
 
 
14
import pygtk
 
15
pygtk.require("2.0")
15
16
import gtk
16
17
import pango
 
18
import os
 
19
import re
17
20
import sys
18
21
 
19
22
try:
21
24
    have_gtksourceview = True
22
25
except ImportError:
23
26
    have_gtksourceview = False
24
 
 
25
 
import bzrlib
26
 
 
27
 
from bzrlib.diff import show_diff_trees
 
27
try:
 
28
    import gconf
 
29
    have_gconf = True
 
30
except ImportError:
 
31
    have_gconf = False
 
32
 
 
33
from bzrlib import merge as _mod_merge, osutils, progress, workingtree
 
34
from bzrlib.diff import show_diff_trees, internal_diff
28
35
from bzrlib.errors import NoSuchFile
29
 
 
30
 
 
31
 
class DiffWindow(gtk.Window):
32
 
    """Diff window.
33
 
 
34
 
    This object represents and manages a single window containing the
35
 
    differences between two revisions on a branch.
36
 
    """
 
36
from bzrlib.patches import parse_patches
 
37
from bzrlib.trace import warning
 
38
from bzrlib.plugins.gtk.window import Window
 
39
from dialog import error_dialog, info_dialog, warning_dialog
 
40
 
 
41
 
 
42
class SelectCancelled(Exception):
 
43
 
 
44
    pass
 
45
 
 
46
 
 
47
class DiffFileView(gtk.ScrolledWindow):
37
48
 
38
49
    def __init__(self):
39
 
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
40
 
        self.set_border_width(0)
41
 
        self.set_title("bzrk diff")
42
 
 
43
 
        # Use two thirds of the screen by default
44
 
        screen = self.get_screen()
45
 
        monitor = screen.get_monitor_geometry(0)
46
 
        width = int(monitor.width * 0.66)
47
 
        height = int(monitor.height * 0.66)
48
 
        self.set_default_size(width, height)
49
 
 
 
50
        gtk.ScrolledWindow.__init__(self)
50
51
        self.construct()
 
52
        self._diffs = {}
51
53
 
52
54
    def construct(self):
53
 
        """Construct the window contents."""
54
 
        # The   window  consists  of   a  pane   containing:  the
55
 
        # hierarchical list  of files on  the left, and  the diff
56
 
        # for the currently selected file on the right.
57
 
        pane = gtk.HPaned()
58
 
        self.add(pane)
59
 
        pane.show()
 
55
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 
56
        self.set_shadow_type(gtk.SHADOW_IN)
 
57
 
 
58
        if have_gtksourceview:
 
59
            self.buffer = gtksourceview.SourceBuffer()
 
60
            slm = gtksourceview.SourceLanguagesManager()
 
61
            gsl = slm.get_language_from_mime_type("text/x-patch")
 
62
            if have_gconf:
 
63
                self.apply_gedit_colors(gsl)
 
64
            self.apply_colordiff_colors(gsl)
 
65
            self.buffer.set_language(gsl)
 
66
            self.buffer.set_highlight(True)
 
67
 
 
68
            sourceview = gtksourceview.SourceView(self.buffer)
 
69
        else:
 
70
            self.buffer = gtk.TextBuffer()
 
71
            sourceview = gtk.TextView(self.buffer)
 
72
 
 
73
        sourceview.set_editable(False)
 
74
        sourceview.modify_font(pango.FontDescription("Monospace"))
 
75
        self.add(sourceview)
 
76
        sourceview.show()
 
77
 
 
78
    @staticmethod
 
79
    def apply_gedit_colors(lang):
 
80
        """Set style for lang to that specified in gedit configuration.
 
81
 
 
82
        This method needs the gconf module.
 
83
 
 
84
        :param lang: a gtksourceview.SourceLanguage object.
 
85
        """
 
86
        GEDIT_SYNTAX_PATH = '/apps/gedit-2/preferences/syntax_highlighting'
 
87
        GEDIT_LANG_PATH = GEDIT_SYNTAX_PATH + '/' + lang.get_id()
 
88
 
 
89
        client = gconf.client_get_default()
 
90
        client.add_dir(GEDIT_LANG_PATH, gconf.CLIENT_PRELOAD_NONE)
 
91
 
 
92
        for tag in lang.get_tags():
 
93
            tag_id = tag.get_id()
 
94
            gconf_key = GEDIT_LANG_PATH + '/' + tag_id
 
95
            style_string = client.get_string(gconf_key)
 
96
 
 
97
            if style_string is None:
 
98
                continue
 
99
 
 
100
            # function to get a bool from a string that's either '0' or '1'
 
101
            string_bool = lambda x: bool(int(x))
 
102
 
 
103
            # style_string is a string like "2/#FFCCAA/#000000/0/1/0/0"
 
104
            # values are: mask, fg, bg, italic, bold, underline, strike
 
105
            # this packs them into (str_value, attr_name, conv_func) tuples
 
106
            items = zip(style_string.split('/'), ['mask', 'foreground',
 
107
                'background', 'italic', 'bold', 'underline', 'strikethrough' ],
 
108
                [ int, gtk.gdk.color_parse, gtk.gdk.color_parse, string_bool,
 
109
                    string_bool, string_bool, string_bool ]
 
110
            )
 
111
 
 
112
            style = gtksourceview.SourceTagStyle()
 
113
 
 
114
            # XXX The mask attribute controls whether the present values of
 
115
            # foreground and background color should in fact be used. Ideally
 
116
            # (and that's what gedit does), one could set all three attributes,
 
117
            # and let the TagStyle object figure out which colors to use.
 
118
            # However, in the GtkSourceview python bindings, the mask attribute
 
119
            # is read-only, and it's derived instead from the colors being
 
120
            # set or not. This means that we have to sometimes refrain from
 
121
            # setting fg or bg colors, depending on the value of the mask.
 
122
            # This code could go away if mask were writable.
 
123
            mask = int(items[0][0])
 
124
            if not (mask & 1): # GTK_SOURCE_TAG_STYLE_USE_BACKGROUND
 
125
                items[2:3] = []
 
126
            if not (mask & 2): # GTK_SOURCE_TAG_STYLE_USE_FOREGROUND
 
127
                items[1:2] = []
 
128
            items[0:1] = [] # skip the mask unconditionally
 
129
 
 
130
            for value, attr, func in items:
 
131
                try:
 
132
                    value = func(value)
 
133
                except ValueError:
 
134
                    warning('gconf key %s contains an invalid value: %s'
 
135
                            % gconf_key, value)
 
136
                else:
 
137
                    setattr(style, attr, value)
 
138
 
 
139
            lang.set_tag_style(tag_id, style)
 
140
 
 
141
    @classmethod
 
142
    def apply_colordiff_colors(klass, lang):
 
143
        """Set style colors for lang using the colordiff configuration file.
 
144
 
 
145
        Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
 
146
 
 
147
        :param lang: a "Diff" gtksourceview.SourceLanguage object.
 
148
        """
 
149
        colors = {}
 
150
 
 
151
        for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
 
152
            f = os.path.expanduser(f)
 
153
            if os.path.exists(f):
 
154
                try:
 
155
                    f = file(f)
 
156
                except IOError, e:
 
157
                    warning('could not open file %s: %s' % (f, str(e)))
 
158
                else:
 
159
                    colors.update(klass.parse_colordiffrc(f))
 
160
                    f.close()
 
161
 
 
162
        if not colors:
 
163
            # ~/.colordiffrc does not exist
 
164
            return
 
165
 
 
166
        mapping = {
 
167
                # map GtkSourceView tags to colordiff names
 
168
                # since GSV is richer, accept new names for extra bits,
 
169
                # defaulting to old names if they're not present
 
170
                'Added@32@line': ['newtext'],
 
171
                'Removed@32@line': ['oldtext'],
 
172
                'Location': ['location', 'diffstuff'],
 
173
                'Diff@32@file': ['file', 'diffstuff'],
 
174
                'Special@32@case': ['specialcase', 'diffstuff'],
 
175
        }
 
176
 
 
177
        for tag in lang.get_tags():
 
178
            tag_id = tag.get_id()
 
179
            keys = mapping.get(tag_id, [])
 
180
            color = None
 
181
 
 
182
            for key in keys:
 
183
                color = colors.get(key, None)
 
184
                if color is not None:
 
185
                    break
 
186
 
 
187
            if color is None:
 
188
                continue
 
189
 
 
190
            style = gtksourceview.SourceTagStyle()
 
191
            try:
 
192
                style.foreground = gtk.gdk.color_parse(color)
 
193
            except ValueError:
 
194
                warning('not a valid color: %s' % color)
 
195
            else:
 
196
                lang.set_tag_style(tag_id, style)
 
197
 
 
198
    @staticmethod
 
199
    def parse_colordiffrc(fileobj):
 
200
        """Parse fileobj as a colordiff configuration file.
 
201
 
 
202
        :return: A dict with the key -> value pairs.
 
203
        """
 
204
        colors = {}
 
205
        for line in fileobj:
 
206
            if re.match(r'^\s*#', line):
 
207
                continue
 
208
            if '=' not in line:
 
209
                continue
 
210
            key, val = line.split('=', 1)
 
211
            colors[key.strip()] = val.strip()
 
212
        return colors
 
213
 
 
214
    def set_trees(self, rev_tree, parent_tree):
 
215
        self.rev_tree = rev_tree
 
216
        self.parent_tree = parent_tree
 
217
#        self._build_delta()
 
218
 
 
219
#    def _build_delta(self):
 
220
#        self.parent_tree.lock_read()
 
221
#        self.rev_tree.lock_read()
 
222
#        try:
 
223
#            self.delta = _iter_changes_to_status(self.parent_tree, self.rev_tree)
 
224
#            self.path_to_status = {}
 
225
#            self.path_to_diff = {}
 
226
#            source_inv = self.parent_tree.inventory
 
227
#            target_inv = self.rev_tree.inventory
 
228
#            for (file_id, real_path, change_type, display_path) in self.delta:
 
229
#                self.path_to_status[real_path] = u'=== %s %s' % (change_type, display_path)
 
230
#                if change_type in ('modified', 'renamed and modified'):
 
231
#                    source_ie = source_inv[file_id]
 
232
#                    target_ie = target_inv[file_id]
 
233
#                    sio = StringIO()
 
234
#                    source_ie.diff(internal_diff, *old path, *old_tree,
 
235
#                                   *new_path, target_ie, self.rev_tree,
 
236
#                                   sio)
 
237
#                    self.path_to_diff[real_path] = 
 
238
#
 
239
#        finally:
 
240
#            self.rev_tree.unlock()
 
241
#            self.parent_tree.unlock()
 
242
 
 
243
    def show_diff(self, specific_files):
 
244
        sections = []
 
245
        if specific_files is None:
 
246
            self.buffer.set_text(self._diffs[None])
 
247
        else:
 
248
            for specific_file in specific_files:
 
249
                sections.append(self._diffs[specific_file])
 
250
            self.buffer.set_text(''.join(sections))
 
251
 
 
252
 
 
253
class DiffView(DiffFileView):
 
254
    """This is the soft and chewy filling for a DiffWindow."""
 
255
 
 
256
    def __init__(self):
 
257
        DiffFileView.__init__(self)
 
258
        self.rev_tree = None
 
259
        self.parent_tree = None
 
260
 
 
261
    def show_diff(self, specific_files):
 
262
        s = StringIO()
 
263
        show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files,
 
264
                        old_label='', new_label='',
 
265
                        # path_encoding=sys.getdefaultencoding()
 
266
                        # The default is utf-8, but we interpret the file
 
267
                        # contents as getdefaultencoding(), so we should
 
268
                        # probably try to make the paths in the same encoding.
 
269
                        )
 
270
        # str.decode(encoding, 'replace') doesn't do anything. Because if a
 
271
        # character is not valid in 'encoding' there is nothing to replace, the
 
272
        # 'replace' is for 'str.encode()'
 
273
        try:
 
274
            decoded = s.getvalue().decode(sys.getdefaultencoding())
 
275
        except UnicodeDecodeError:
 
276
            try:
 
277
                decoded = s.getvalue().decode('UTF-8')
 
278
            except UnicodeDecodeError:
 
279
                decoded = s.getvalue().decode('iso-8859-1')
 
280
                # This always works, because every byte has a valid
 
281
                # mapping from iso-8859-1 to Unicode
 
282
        # TextBuffer must contain pure UTF-8 data
 
283
        self.buffer.set_text(decoded.encode('UTF-8'))
 
284
 
 
285
 
 
286
class DiffWidget(gtk.HPaned):
 
287
    """Diff widget
 
288
 
 
289
    """
 
290
    def __init__(self):
 
291
        super(DiffWidget, self).__init__()
60
292
 
61
293
        # The file hierarchy: a scrollable treeview
62
294
        scrollwin = gtk.ScrolledWindow()
63
295
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
64
296
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
65
 
        pane.pack1(scrollwin)
 
297
        self.pack1(scrollwin)
66
298
        scrollwin.show()
67
299
 
68
300
        self.model = gtk.TreeStore(str, str)
80
312
        column.add_attribute(cell, "text", 0)
81
313
        self.treeview.append_column(column)
82
314
 
 
315
    def set_diff_text(self, lines):
83
316
        # The diffs of the  selected file: a scrollable source or
84
317
        # text view
85
 
        scrollwin = gtk.ScrolledWindow()
86
 
        scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
87
 
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
88
 
        pane.pack2(scrollwin)
89
 
        scrollwin.show()
90
 
 
91
 
        if have_gtksourceview:
92
 
            self.buffer = gtksourceview.SourceBuffer()
93
 
            slm = gtksourceview.SourceLanguagesManager()
94
 
            gsl = slm.get_language_from_mime_type("text/x-patch")
95
 
            self.buffer.set_language(gsl)
96
 
            self.buffer.set_highlight(True)
97
 
 
98
 
            sourceview = gtksourceview.SourceView(self.buffer)
99
 
        else:
100
 
            self.buffer = gtk.TextBuffer()
101
 
            sourceview = gtk.TextView(self.buffer)
102
 
 
103
 
        sourceview.set_editable(False)
104
 
        sourceview.modify_font(pango.FontDescription("Monospace"))
105
 
        scrollwin.add(sourceview)
106
 
        sourceview.show()
107
 
 
108
 
    def set_diff(self, description, rev_tree, parent_tree):
 
318
        self.diff_view = DiffFileView()
 
319
        self.diff_view.show()
 
320
        self.pack2(self.diff_view)
 
321
        self.model.append(None, [ "Complete Diff", "" ])
 
322
        self.diff_view._diffs[None] = ''.join(lines)
 
323
        for patch in parse_patches(lines):
 
324
            oldname = patch.oldname.split('\t')[0]
 
325
            newname = patch.newname.split('\t')[0]
 
326
            self.model.append(None, [oldname, newname])
 
327
            self.diff_view._diffs[newname] = str(patch)
 
328
        self.diff_view.show_diff(None)
 
329
 
 
330
    def set_diff(self, rev_tree, parent_tree):
109
331
        """Set the differences showed by this window.
110
332
 
111
333
        Compares the two trees and populates the window with the
112
334
        differences.
113
335
        """
 
336
        self.diff_view = DiffView()
 
337
        self.pack2(self.diff_view)
 
338
        self.diff_view.show()
 
339
        self.diff_view.set_trees(rev_tree, parent_tree)
114
340
        self.rev_tree = rev_tree
115
341
        self.parent_tree = parent_tree
116
342
 
141
367
                self.model.append(titer, [ path, path ])
142
368
 
143
369
        self.treeview.expand_all()
144
 
        self.set_title(description + " - bzrk diff")
145
370
 
146
371
    def set_file(self, file_path):
147
372
        tv_path = None
162
387
        if specific_files == [ None ]:
163
388
            return
164
389
        elif specific_files == [ "" ]:
165
 
            specific_files = []
166
 
 
167
 
        s = StringIO()
168
 
        show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files)
169
 
        self.buffer.set_text(s.getvalue().decode(sys.getdefaultencoding(), 'replace'))
 
390
            specific_files = None
 
391
 
 
392
        self.diff_view.show_diff(specific_files)
 
393
 
 
394
 
 
395
class DiffWindow(Window):
 
396
    """Diff window.
 
397
 
 
398
    This object represents and manages a single window containing the
 
399
    differences between two revisions on a branch.
 
400
    """
 
401
 
 
402
    def __init__(self, parent=None):
 
403
        Window.__init__(self, parent)
 
404
        self.set_border_width(0)
 
405
        self.set_title("bzrk diff")
 
406
 
 
407
        # Use two thirds of the screen by default
 
408
        screen = self.get_screen()
 
409
        monitor = screen.get_monitor_geometry(0)
 
410
        width = int(monitor.width * 0.66)
 
411
        height = int(monitor.height * 0.66)
 
412
        self.set_default_size(width, height)
 
413
 
 
414
        self.construct()
 
415
 
 
416
    def construct(self):
 
417
        """Construct the window contents."""
 
418
        self.vbox = gtk.VBox()
 
419
        self.add(self.vbox)
 
420
        self.vbox.show()
 
421
        hbox = self._get_button_bar()
 
422
        if hbox is not None:
 
423
            self.vbox.pack_start(hbox, expand=False, fill=True)
 
424
        self.diff = DiffWidget()
 
425
        self.vbox.add(self.diff)
 
426
        self.diff.show_all()
 
427
 
 
428
    def _get_button_bar(self):
 
429
        return None
 
430
 
 
431
    def set_diff_text(self, description, lines):
 
432
        self.diff.set_diff_text(lines)
 
433
        self.set_title(description + " - bzrk diff")
 
434
 
 
435
    def set_diff(self, description, rev_tree, parent_tree):
 
436
        """Set the differences showed by this window.
 
437
 
 
438
        Compares the two trees and populates the window with the
 
439
        differences.
 
440
        """
 
441
        self.diff.set_diff(rev_tree, parent_tree)
 
442
        self.set_title(description + " - bzrk diff")
 
443
 
 
444
    def set_file(self, file_path):
 
445
        self.diff.set_file(file_path)
 
446
 
 
447
 
 
448
class MergeDirectiveWindow(DiffWindow):
 
449
 
 
450
    def __init__(self, directive, parent=None):
 
451
        DiffWindow.__init__(self, parent)
 
452
        self._merge_target = None
 
453
        self.directive = directive
 
454
 
 
455
    def _get_button_bar(self):
 
456
        merge_button = gtk.Button('Merge')
 
457
        merge_button.show()
 
458
        merge_button.set_relief(gtk.RELIEF_NONE)
 
459
        merge_button.connect("clicked", self.perform_merge)
 
460
 
 
461
        hbox = gtk.HButtonBox()
 
462
        hbox.set_layout(gtk.BUTTONBOX_START)
 
463
        hbox.pack_start(merge_button, expand=False, fill=True)
 
464
        hbox.show()
 
465
        return hbox
 
466
 
 
467
    def perform_merge(self, window):
 
468
        try:
 
469
            tree = self._get_merge_target()
 
470
        except SelectCancelled:
 
471
            return
 
472
        tree.lock_write()
 
473
        try:
 
474
            try:
 
475
                merger, verified = _mod_merge.Merger.from_mergeable(tree,
 
476
                    self.directive, progress.DummyProgress())
 
477
                merger.check_basis(True)
 
478
                merger.merge_type = _mod_merge.Merge3Merger
 
479
                conflict_count = merger.do_merge()
 
480
                merger.set_pending()
 
481
                if conflict_count == 0:
 
482
                    # No conflicts found.
 
483
                    info_dialog(_('Merge successful'),
 
484
                                _('All changes applied successfully.'))
 
485
                else:
 
486
                    # There are conflicts to be resolved.
 
487
                    warning_dialog(_('Conflicts encountered'),
 
488
                                   _('Please resolve the conflicts manually'
 
489
                                     ' before committing.'))
 
490
                self.destroy()
 
491
            except Exception, e:
 
492
                error_dialog('Error', str(e))
 
493
        finally:
 
494
            tree.unlock()
 
495
 
 
496
    def _get_merge_target(self):
 
497
        if self._merge_target is not None:
 
498
            return self._merge_target
 
499
        d = gtk.FileChooserDialog('Merge branch', self,
 
500
                                  gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
 
501
                                  buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
 
502
                                           gtk.STOCK_CANCEL,
 
503
                                           gtk.RESPONSE_CANCEL,))
 
504
        try:
 
505
            result = d.run()
 
506
            if result == gtk.RESPONSE_OK:
 
507
                uri = d.get_current_folder_uri()
 
508
                return workingtree.WorkingTree.open(uri)
 
509
            else:
 
510
                raise SelectCancelled()
 
511
        finally:
 
512
            d.destroy()
 
513
 
 
514
 
 
515
def _iter_changes_to_status(source, target):
 
516
    """Determine the differences between trees.
 
517
 
 
518
    This is a wrapper around _iter_changes which just yields more
 
519
    understandable results.
 
520
 
 
521
    :param source: The source tree (basis tree)
 
522
    :param target: The target tree
 
523
    :return: A list of (file_id, real_path, change_type, display_path)
 
524
    """
 
525
    added = 'added'
 
526
    removed = 'removed'
 
527
    renamed = 'renamed'
 
528
    renamed_and_modified = 'renamed and modified'
 
529
    modified = 'modified'
 
530
    kind_changed = 'kind changed'
 
531
 
 
532
    # TODO: Handle metadata changes
 
533
 
 
534
    status = []
 
535
    target.lock_read()
 
536
    try:
 
537
        source.lock_read()
 
538
        try:
 
539
            for (file_id, paths, changed_content, versioned, parent_ids, names,
 
540
                 kinds, executables) in target._iter_changes(source):
 
541
 
 
542
                # Skip the root entry if it isn't very interesting
 
543
                if parent_ids == (None, None):
 
544
                    continue
 
545
 
 
546
                change_type = None
 
547
                if kinds[0] is None:
 
548
                    source_marker = ''
 
549
                else:
 
550
                    source_marker = osutils.kind_marker(kinds[0])
 
551
                if kinds[1] is None:
 
552
                    assert kinds[0] is not None
 
553
                    marker = osutils.kind_marker(kinds[0])
 
554
                else:
 
555
                    marker = osutils.kind_marker(kinds[1])
 
556
 
 
557
                real_path = paths[1]
 
558
                if real_path is None:
 
559
                    real_path = paths[0]
 
560
                assert real_path is not None
 
561
                display_path = real_path + marker
 
562
 
 
563
                present_source = versioned[0] and kinds[0] is not None
 
564
                present_target = versioned[1] and kinds[1] is not None
 
565
 
 
566
                if present_source != present_target:
 
567
                    if present_target:
 
568
                        change_type = added
 
569
                    else:
 
570
                        assert present_source
 
571
                        change_type = removed
 
572
                elif names[0] != names[1] or parent_ids[0] != parent_ids[1]:
 
573
                    # Renamed
 
574
                    if changed_content or executables[0] != executables[1]:
 
575
                        # and modified
 
576
                        change_type = renamed_and_modified
 
577
                    else:
 
578
                        change_type = renamed
 
579
                    display_path = (paths[0] + source_marker
 
580
                                    + ' => ' + paths[1] + marker)
 
581
                elif kinds[0] != kinds[1]:
 
582
                    change_type = kind_changed
 
583
                    display_path = (paths[0] + source_marker
 
584
                                    + ' => ' + paths[1] + marker)
 
585
                elif changed_content is True or executables[0] != executables[1]:
 
586
                    change_type = modified
 
587
                else:
 
588
                    assert False, "How did we get here?"
 
589
 
 
590
                status.append((file_id, real_path, change_type, display_path))
 
591
        finally:
 
592
            source.unlock()
 
593
    finally:
 
594
        target.unlock()
 
595
 
 
596
    return status