/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: Jelmer Vernooij
  • Date: 2009-06-05 12:18:50 UTC
  • Revision ID: jelmer@samba.org-20090605121850-d139huhca7le1sxp
Use get_apparent_authors() rather than deprecated get_apparent_author().

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""Difference window.
 
2
 
 
3
This module contains the code to manage the diff window which shows
 
4
the changes made between two revisions on a branch.
 
5
"""
 
6
 
 
7
__copyright__ = "Copyright 2005 Canonical Ltd."
 
8
__author__    = "Scott James Remnant <scott@ubuntu.com>"
 
9
 
 
10
 
 
11
from cStringIO import StringIO
 
12
 
 
13
import pygtk
 
14
pygtk.require("2.0")
 
15
import gtk
 
16
import pango
 
17
import os
 
18
import re
 
19
import sys
 
20
from xml.etree.ElementTree import Element, SubElement, tostring
 
21
 
 
22
try:
 
23
    import gtksourceview2
 
24
    have_gtksourceview = True
 
25
except ImportError:
 
26
    have_gtksourceview = False
 
27
try:
 
28
    import gconf
 
29
    have_gconf = True
 
30
except ImportError:
 
31
    have_gconf = False
 
32
 
 
33
from bzrlib import (
 
34
    merge as _mod_merge,
 
35
    osutils,
 
36
    progress,
 
37
    urlutils,
 
38
    workingtree,
 
39
)
 
40
from bzrlib.diff import show_diff_trees, internal_diff
 
41
from bzrlib.errors import NoSuchFile
 
42
from bzrlib.patches import parse_patches
 
43
from bzrlib.trace import warning
 
44
from bzrlib.plugins.gtk import _i18n
 
45
from bzrlib.plugins.gtk.window import Window
 
46
from dialog import error_dialog, info_dialog, warning_dialog
 
47
 
 
48
 
 
49
class SelectCancelled(Exception):
 
50
 
 
51
    pass
 
52
 
 
53
 
 
54
class DiffFileView(gtk.ScrolledWindow):
 
55
    """Window for displaying diffs from a diff file"""
 
56
 
 
57
    def __init__(self):
 
58
        gtk.ScrolledWindow.__init__(self)
 
59
        self.construct()
 
60
        self._diffs = {}
 
61
 
 
62
    def construct(self):
 
63
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
 
64
        self.set_shadow_type(gtk.SHADOW_IN)
 
65
 
 
66
        if have_gtksourceview:
 
67
            self.buffer = gtksourceview2.Buffer()
 
68
            slm = gtksourceview2.LanguageManager()
 
69
            gsl = slm.guess_language(content_type="text/x-patch")
 
70
            if have_gconf:
 
71
                self.apply_gedit_colors(self.buffer)
 
72
            self.apply_colordiff_colors(self.buffer)
 
73
            self.buffer.set_language(gsl)
 
74
            self.buffer.set_highlight_syntax(True)
 
75
 
 
76
            self.sourceview = gtksourceview2.View(self.buffer)
 
77
        else:
 
78
            self.buffer = gtk.TextBuffer()
 
79
            self.sourceview = gtk.TextView(self.buffer)
 
80
 
 
81
        self.sourceview.set_editable(False)
 
82
        self.sourceview.modify_font(pango.FontDescription("Monospace"))
 
83
        self.add(self.sourceview)
 
84
        self.sourceview.show()
 
85
 
 
86
    @staticmethod
 
87
    def apply_gedit_colors(buf):
 
88
        """Set style to that specified in gedit configuration.
 
89
 
 
90
        This method needs the gconf module.
 
91
 
 
92
        :param buf: a gtksourceview2.Buffer object.
 
93
        """
 
94
        GEDIT_SCHEME_PATH = '/apps/gedit-2/preferences/editor/colors/scheme'
 
95
 
 
96
        client = gconf.client_get_default()
 
97
        style_scheme_name = client.get_string(GEDIT_SCHEME_PATH)
 
98
        style_scheme = gtksourceview2.StyleSchemeManager().get_scheme(style_scheme_name)
 
99
        
 
100
        buf.set_style_scheme(style_scheme)
 
101
 
 
102
    @classmethod
 
103
    def apply_colordiff_colors(klass, buf):
 
104
        """Set style colors for lang using the colordiff configuration file.
 
105
 
 
106
        Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
 
107
 
 
108
        :param buf: a "Diff" gtksourceview2.Buffer object.
 
109
        """
 
110
        scheme_manager = gtksourceview2.StyleSchemeManager()
 
111
        style_scheme = scheme_manager.get_scheme('colordiff')
 
112
        
 
113
        # if style scheme not found, we'll generate it from colordiffrc
 
114
        # TODO: reload if colordiffrc has changed.
 
115
        if style_scheme is None:
 
116
            colors = {}
 
117
 
 
118
            for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
 
119
                f = os.path.expanduser(f)
 
120
                if os.path.exists(f):
 
121
                    try:
 
122
                        f = file(f)
 
123
                    except IOError, e:
 
124
                        warning('could not open file %s: %s' % (f, str(e)))
 
125
                    else:
 
126
                        colors.update(klass.parse_colordiffrc(f))
 
127
                        f.close()
 
128
 
 
129
            if not colors:
 
130
                # ~/.colordiffrc does not exist
 
131
                return
 
132
            
 
133
            mapping = {
 
134
                # map GtkSourceView2 scheme styles to colordiff names
 
135
                # since GSV is richer, accept new names for extra bits,
 
136
                # defaulting to old names if they're not present
 
137
                'diff:added-line': ['newtext'],
 
138
                'diff:removed-line': ['oldtext'],
 
139
                'diff:location': ['location', 'diffstuff'],
 
140
                'diff:file': ['file', 'diffstuff'],
 
141
                'diff:special-case': ['specialcase', 'diffstuff'],
 
142
            }
 
143
            
 
144
            converted_colors = {}
 
145
            for name, values in mapping.items():
 
146
                color = None
 
147
                for value in values:
 
148
                    color = colors.get(value, None)
 
149
                    if color is not None:
 
150
                        break
 
151
                if color is None:
 
152
                    continue
 
153
                converted_colors[name] = color
 
154
            
 
155
            # some xml magic to produce needed style scheme description
 
156
            e_style_scheme = Element('style-scheme')
 
157
            e_style_scheme.set('id', 'colordiff')
 
158
            e_style_scheme.set('_name', 'ColorDiff')
 
159
            e_style_scheme.set('version', '1.0')
 
160
            for name, color in converted_colors.items():
 
161
                style = SubElement(e_style_scheme, 'style')
 
162
                style.set('name', name)
 
163
                style.set('foreground', '#%s' % color)
 
164
            
 
165
            scheme_xml = tostring(e_style_scheme, 'UTF-8')
 
166
            if not os.path.exists(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles')):
 
167
                os.makedirs(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles'))
 
168
            file(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles/colordiff.xml'), 'w').write(scheme_xml)
 
169
            
 
170
            scheme_manager.force_rescan()
 
171
            style_scheme = scheme_manager.get_scheme('colordiff')
 
172
        
 
173
        buf.set_style_scheme(style_scheme)
 
174
 
 
175
    @staticmethod
 
176
    def parse_colordiffrc(fileobj):
 
177
        """Parse fileobj as a colordiff configuration file.
 
178
 
 
179
        :return: A dict with the key -> value pairs.
 
180
        """
 
181
        colors = {}
 
182
        for line in fileobj:
 
183
            if re.match(r'^\s*#', line):
 
184
                continue
 
185
            if '=' not in line:
 
186
                continue
 
187
            key, val = line.split('=', 1)
 
188
            colors[key.strip()] = val.strip()
 
189
        return colors
 
190
 
 
191
    def set_trees(self, rev_tree, parent_tree):
 
192
        self.rev_tree = rev_tree
 
193
        self.parent_tree = parent_tree
 
194
#        self._build_delta()
 
195
 
 
196
#    def _build_delta(self):
 
197
#        self.parent_tree.lock_read()
 
198
#        self.rev_tree.lock_read()
 
199
#        try:
 
200
#            self.delta = iter_changes_to_status(self.parent_tree, self.rev_tree)
 
201
#            self.path_to_status = {}
 
202
#            self.path_to_diff = {}
 
203
#            source_inv = self.parent_tree.inventory
 
204
#            target_inv = self.rev_tree.inventory
 
205
#            for (file_id, real_path, change_type, display_path) in self.delta:
 
206
#                self.path_to_status[real_path] = u'=== %s %s' % (change_type, display_path)
 
207
#                if change_type in ('modified', 'renamed and modified'):
 
208
#                    source_ie = source_inv[file_id]
 
209
#                    target_ie = target_inv[file_id]
 
210
#                    sio = StringIO()
 
211
#                    source_ie.diff(internal_diff, *old path, *old_tree,
 
212
#                                   *new_path, target_ie, self.rev_tree,
 
213
#                                   sio)
 
214
#                    self.path_to_diff[real_path] = 
 
215
#
 
216
#        finally:
 
217
#            self.rev_tree.unlock()
 
218
#            self.parent_tree.unlock()
 
219
 
 
220
    def show_diff(self, specific_files):
 
221
        sections = []
 
222
        if specific_files is None:
 
223
            self.buffer.set_text(self._diffs[None])
 
224
        else:
 
225
            for specific_file in specific_files:
 
226
                sections.append(self._diffs[specific_file])
 
227
            self.buffer.set_text(''.join(sections))
 
228
 
 
229
 
 
230
class DiffView(DiffFileView):
 
231
    """This is the soft and chewy filling for a DiffWindow."""
 
232
 
 
233
    def __init__(self):
 
234
        DiffFileView.__init__(self)
 
235
        self.rev_tree = None
 
236
        self.parent_tree = None
 
237
 
 
238
    def show_diff(self, specific_files):
 
239
        """Show the diff for the specified files"""
 
240
        s = StringIO()
 
241
        show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files,
 
242
                        old_label='', new_label='',
 
243
                        # path_encoding=sys.getdefaultencoding()
 
244
                        # The default is utf-8, but we interpret the file
 
245
                        # contents as getdefaultencoding(), so we should
 
246
                        # probably try to make the paths in the same encoding.
 
247
                        )
 
248
        # str.decode(encoding, 'replace') doesn't do anything. Because if a
 
249
        # character is not valid in 'encoding' there is nothing to replace, the
 
250
        # 'replace' is for 'str.encode()'
 
251
        try:
 
252
            decoded = s.getvalue().decode(sys.getdefaultencoding())
 
253
        except UnicodeDecodeError:
 
254
            try:
 
255
                decoded = s.getvalue().decode('UTF-8')
 
256
            except UnicodeDecodeError:
 
257
                decoded = s.getvalue().decode('iso-8859-1')
 
258
                # This always works, because every byte has a valid
 
259
                # mapping from iso-8859-1 to Unicode
 
260
        # TextBuffer must contain pure UTF-8 data
 
261
        self.buffer.set_text(decoded.encode('UTF-8'))
 
262
 
 
263
 
 
264
class DiffWidget(gtk.HPaned):
 
265
    """Diff widget
 
266
 
 
267
    """
 
268
    def __init__(self):
 
269
        super(DiffWidget, self).__init__()
 
270
 
 
271
        # The file hierarchy: a scrollable treeview
 
272
        scrollwin = gtk.ScrolledWindow()
 
273
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
 
274
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
 
275
        self.pack1(scrollwin)
 
276
        scrollwin.show()
 
277
        
 
278
        self.model = gtk.TreeStore(str, str)
 
279
        self.treeview = gtk.TreeView(self.model)
 
280
        self.treeview.set_headers_visible(False)
 
281
        self.treeview.set_search_column(1)
 
282
        self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
 
283
        scrollwin.add(self.treeview)
 
284
        self.treeview.show()
 
285
 
 
286
        cell = gtk.CellRendererText()
 
287
        cell.set_property("width-chars", 20)
 
288
        column = gtk.TreeViewColumn()
 
289
        column.pack_start(cell, expand=True)
 
290
        column.add_attribute(cell, "text", 0)
 
291
        self.treeview.append_column(column)
 
292
 
 
293
    def set_diff_text(self, lines):
 
294
        """Set the current diff from a list of lines
 
295
 
 
296
        :param lines: The diff to show, in unified diff format
 
297
        """
 
298
        # The diffs of the  selected file: a scrollable source or
 
299
        # text view
 
300
 
 
301
    def set_diff_text_sections(self, sections):
 
302
        if getattr(self, 'diff_view', None) is None:
 
303
            self.diff_view = DiffFileView()
 
304
            self.pack2(self.diff_view)
 
305
        self.diff_view.show()
 
306
        for oldname, newname, patch in sections:
 
307
            self.diff_view._diffs[newname] = str(patch)
 
308
            if newname is None:
 
309
                newname = ''
 
310
            self.model.append(None, [oldname, newname])
 
311
        self.diff_view.show_diff(None)
 
312
 
 
313
    def set_diff(self, rev_tree, parent_tree):
 
314
        """Set the differences showed by this window.
 
315
 
 
316
        Compares the two trees and populates the window with the
 
317
        differences.
 
318
        """
 
319
        if getattr(self, 'diff_view', None) is None:
 
320
            self.diff_view = DiffView()
 
321
            self.pack2(self.diff_view)
 
322
        self.diff_view.show()
 
323
        self.diff_view.set_trees(rev_tree, parent_tree)
 
324
        self.rev_tree = rev_tree
 
325
        self.parent_tree = parent_tree
 
326
 
 
327
        self.model.clear()
 
328
        delta = self.rev_tree.changes_from(self.parent_tree)
 
329
 
 
330
        self.model.append(None, [ "Complete Diff", "" ])
 
331
 
 
332
        if len(delta.added):
 
333
            titer = self.model.append(None, [ "Added", None ])
 
334
            for path, id, kind in delta.added:
 
335
                self.model.append(titer, [ path, path ])
 
336
 
 
337
        if len(delta.removed):
 
338
            titer = self.model.append(None, [ "Removed", None ])
 
339
            for path, id, kind in delta.removed:
 
340
                self.model.append(titer, [ path, path ])
 
341
 
 
342
        if len(delta.renamed):
 
343
            titer = self.model.append(None, [ "Renamed", None ])
 
344
            for oldpath, newpath, id, kind, text_modified, meta_modified \
 
345
                    in delta.renamed:
 
346
                self.model.append(titer, [ oldpath, newpath ])
 
347
 
 
348
        if len(delta.modified):
 
349
            titer = self.model.append(None, [ "Modified", None ])
 
350
            for path, id, kind, text_modified, meta_modified in delta.modified:
 
351
                self.model.append(titer, [ path, path ])
 
352
 
 
353
        self.treeview.expand_all()
 
354
        self.diff_view.show_diff(None)
 
355
 
 
356
    def set_file(self, file_path):
 
357
        """Select the current file to display"""
 
358
        tv_path = None
 
359
        for data in self.model:
 
360
            for child in data.iterchildren():
 
361
                if child[0] == file_path or child[1] == file_path:
 
362
                    tv_path = child.path
 
363
                    break
 
364
        if tv_path is None:
 
365
            raise NoSuchFile(file_path)
 
366
        self.treeview.set_cursor(tv_path)
 
367
        self.treeview.scroll_to_cell(tv_path)
 
368
 
 
369
    def _treeview_cursor_cb(self, *args):
 
370
        """Callback for when the treeview cursor changes."""
 
371
        (path, col) = self.treeview.get_cursor()
 
372
        specific_files = [ self.model[path][1] ]
 
373
        if specific_files == [ None ]:
 
374
            return
 
375
        elif specific_files == [ "" ]:
 
376
            specific_files = None
 
377
        
 
378
        self.diff_view.show_diff(specific_files)
 
379
    
 
380
    def _on_wraplines_toggled(self, widget=None, wrap=False):
 
381
        """Callback for when the wrap lines checkbutton is toggled"""
 
382
        if wrap or widget.get_active():
 
383
            self.diff_view.sourceview.set_wrap_mode(gtk.WRAP_WORD)
 
384
        else:
 
385
            self.diff_view.sourceview.set_wrap_mode(gtk.WRAP_NONE)
 
386
 
 
387
class DiffWindow(Window):
 
388
    """Diff window.
 
389
 
 
390
    This object represents and manages a single window containing the
 
391
    differences between two revisions on a branch.
 
392
    """
 
393
 
 
394
    def __init__(self, parent=None, operations=None):
 
395
        Window.__init__(self, parent)
 
396
        self.set_border_width(0)
 
397
        self.set_title("bzrk diff")
 
398
 
 
399
        # Use two thirds of the screen by default
 
400
        screen = self.get_screen()
 
401
        monitor = screen.get_monitor_geometry(0)
 
402
        width = int(monitor.width * 0.66)
 
403
        height = int(monitor.height * 0.66)
 
404
        self.set_default_size(width, height)
 
405
        self.construct(operations)
 
406
 
 
407
    def construct(self, operations):
 
408
        """Construct the window contents."""
 
409
        self.vbox = gtk.VBox()
 
410
        self.add(self.vbox)
 
411
        self.vbox.show()
 
412
        self.diff = DiffWidget()
 
413
        self.vbox.pack_end(self.diff, True, True, 0)
 
414
        self.diff.show_all()
 
415
        # Build after DiffWidget to connect signals
 
416
        menubar = self._get_menu_bar()
 
417
        self.vbox.pack_start(menubar, False, False, 0)
 
418
        hbox = self._get_button_bar(operations)
 
419
        if hbox is not None:
 
420
            self.vbox.pack_start(hbox, False, True, 0)
 
421
        
 
422
    
 
423
    def _get_menu_bar(self):
 
424
        menubar = gtk.MenuBar()
 
425
        # View menu
 
426
        mb_view = gtk.MenuItem(_i18n("_View"))
 
427
        mb_view_menu = gtk.Menu()
 
428
        mb_view_wrapsource = gtk.CheckMenuItem(_i18n("Wrap _Long Lines"))
 
429
        mb_view_wrapsource.connect('activate', self.diff._on_wraplines_toggled)
 
430
        mb_view_wrapsource.show()
 
431
        mb_view_menu.append(mb_view_wrapsource)
 
432
        mb_view.show()
 
433
        mb_view.set_submenu(mb_view_menu)
 
434
        mb_view.show()
 
435
        menubar.append(mb_view)
 
436
        menubar.show()
 
437
        return menubar
 
438
    
 
439
    def _get_button_bar(self, operations):
 
440
        """Return a button bar to use.
 
441
 
 
442
        :return: None, meaning that no button bar will be used.
 
443
        """
 
444
        if operations is None:
 
445
            return None
 
446
        hbox = gtk.HButtonBox()
 
447
        hbox.set_layout(gtk.BUTTONBOX_START)
 
448
        for title, method in operations:
 
449
            merge_button = gtk.Button(title)
 
450
            merge_button.show()
 
451
            merge_button.set_relief(gtk.RELIEF_NONE)
 
452
            merge_button.connect("clicked", method)
 
453
            hbox.pack_start(merge_button, expand=False, fill=True)
 
454
        hbox.show()
 
455
        return hbox
 
456
 
 
457
    def _get_merge_target(self):
 
458
        d = gtk.FileChooserDialog('Merge branch', self,
 
459
                                  gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
 
460
                                  buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
 
461
                                           gtk.STOCK_CANCEL,
 
462
                                           gtk.RESPONSE_CANCEL,))
 
463
        try:
 
464
            result = d.run()
 
465
            if result != gtk.RESPONSE_OK:
 
466
                raise SelectCancelled()
 
467
            return d.get_current_folder_uri()
 
468
        finally:
 
469
            d.destroy()
 
470
 
 
471
    def _merge_successful(self):
 
472
        # No conflicts found.
 
473
        info_dialog(_i18n('Merge successful'),
 
474
                    _i18n('All changes applied successfully.'))
 
475
 
 
476
    def _conflicts(self):
 
477
        warning_dialog(_i18n('Conflicts encountered'),
 
478
                       _i18n('Please resolve the conflicts manually'
 
479
                             ' before committing.'))
 
480
 
 
481
    def _handle_error(self, e):
 
482
        error_dialog('Error', str(e))
 
483
 
 
484
    def _get_save_path(self, basename):
 
485
        d = gtk.FileChooserDialog('Save As', self,
 
486
                                  gtk.FILE_CHOOSER_ACTION_SAVE,
 
487
                                  buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
 
488
                                           gtk.STOCK_CANCEL,
 
489
                                           gtk.RESPONSE_CANCEL,))
 
490
        d.set_current_name(basename)
 
491
        try:
 
492
            result = d.run()
 
493
            if result != gtk.RESPONSE_OK:
 
494
                raise SelectCancelled()
 
495
            return urlutils.local_path_from_url(d.get_uri())
 
496
        finally:
 
497
            d.destroy()
 
498
 
 
499
    def set_diff(self, description, rev_tree, parent_tree):
 
500
        """Set the differences showed by this window.
 
501
 
 
502
        Compares the two trees and populates the window with the
 
503
        differences.
 
504
        """
 
505
        self.diff.set_diff(rev_tree, parent_tree)
 
506
        self.set_title(description + " - bzrk diff")
 
507
 
 
508
    def set_file(self, file_path):
 
509
        self.diff.set_file(file_path)
 
510
 
 
511
 
 
512
class DiffController(object):
 
513
 
 
514
    def __init__(self, path, patch, window=None):
 
515
        self.path = path
 
516
        self.patch = patch
 
517
        if window is None:
 
518
            window = DiffWindow(operations=self._provide_operations())
 
519
            self.initialize_window(window)
 
520
        self.window = window
 
521
 
 
522
    def initialize_window(self, window):
 
523
        window.diff.set_diff_text_sections(self.get_diff_sections())
 
524
        window.set_title(self.path + " - diff")
 
525
 
 
526
    def get_diff_sections(self):
 
527
        yield "Complete Diff", None, ''.join(self.patch)
 
528
        for patch in parse_patches(self.patch):
 
529
            oldname = patch.oldname.split('\t')[0]
 
530
            newname = patch.newname.split('\t')[0]
 
531
            yield oldname, newname, str(patch)
 
532
 
 
533
    def perform_save(self, window):
 
534
        try:
 
535
            save_path = self.window._get_save_path(osutils.basename(self.path))
 
536
        except SelectCancelled:
 
537
            return
 
538
        source = open(self.path, 'rb')
 
539
        try:
 
540
            target = open(save_path, 'wb')
 
541
            try:
 
542
                osutils.pumpfile(source, target)
 
543
            finally:
 
544
                target.close()
 
545
        finally:
 
546
            source.close()
 
547
 
 
548
    def _provide_operations(self):
 
549
        return [('Save', self.perform_save)]
 
550
 
 
551
 
 
552
class MergeDirectiveController(DiffController):
 
553
 
 
554
    def __init__(self, path, directive, window=None):
 
555
        DiffController.__init__(self, path, directive.patch.splitlines(True),
 
556
                                window)
 
557
        self.directive = directive
 
558
        self.merge_target = None
 
559
 
 
560
    def _provide_operations(self):
 
561
        return [('Merge', self.perform_merge), ('Save', self.perform_save)]
 
562
 
 
563
    def perform_merge(self, window):
 
564
        if self.merge_target is None:
 
565
            try:
 
566
                self.merge_target = self.window._get_merge_target()
 
567
            except SelectCancelled:
 
568
                return
 
569
        tree = workingtree.WorkingTree.open(self.merge_target)
 
570
        tree.lock_write()
 
571
        try:
 
572
            try:
 
573
                merger, verified = _mod_merge.Merger.from_mergeable(tree,
 
574
                    self.directive, progress.DummyProgress())
 
575
                merger.check_basis(True)
 
576
                merger.merge_type = _mod_merge.Merge3Merger
 
577
                conflict_count = merger.do_merge()
 
578
                merger.set_pending()
 
579
                if conflict_count == 0:
 
580
                    self.window._merge_successful()
 
581
                else:
 
582
                    self.window._conflicts()
 
583
                    # There are conflicts to be resolved.
 
584
                self.window.destroy()
 
585
            except Exception, e:
 
586
                self.window._handle_error(e)
 
587
        finally:
 
588
            tree.unlock()
 
589
 
 
590
 
 
591
def iter_changes_to_status(source, target):
 
592
    """Determine the differences between trees.
 
593
 
 
594
    This is a wrapper around iter_changes which just yields more
 
595
    understandable results.
 
596
 
 
597
    :param source: The source tree (basis tree)
 
598
    :param target: The target tree
 
599
    :return: A list of (file_id, real_path, change_type, display_path)
 
600
    """
 
601
    added = 'added'
 
602
    removed = 'removed'
 
603
    renamed = 'renamed'
 
604
    renamed_and_modified = 'renamed and modified'
 
605
    modified = 'modified'
 
606
    kind_changed = 'kind changed'
 
607
    missing = 'missing'
 
608
 
 
609
    # TODO: Handle metadata changes
 
610
 
 
611
    status = []
 
612
    target.lock_read()
 
613
    try:
 
614
        source.lock_read()
 
615
        try:
 
616
            for (file_id, paths, changed_content, versioned, parent_ids, names,
 
617
                 kinds, executables) in target.iter_changes(source):
 
618
 
 
619
                # Skip the root entry if it isn't very interesting
 
620
                if parent_ids == (None, None):
 
621
                    continue
 
622
 
 
623
                change_type = None
 
624
                if kinds[0] is None:
 
625
                    source_marker = ''
 
626
                else:
 
627
                    source_marker = osutils.kind_marker(kinds[0])
 
628
 
 
629
                if kinds[1] is None:
 
630
                    if kinds[0] is None:
 
631
                        # We assume bzr will flag only files in that case,
 
632
                        # there may be a bzr bug there as only files seems to
 
633
                        # not receive any kind.
 
634
                        marker = osutils.kind_marker('file')
 
635
                    else:
 
636
                        marker = osutils.kind_marker(kinds[0])
 
637
                else:
 
638
                    marker = osutils.kind_marker(kinds[1])
 
639
 
 
640
                real_path = paths[1]
 
641
                if real_path is None:
 
642
                    real_path = paths[0]
 
643
                assert real_path is not None
 
644
 
 
645
                present_source = versioned[0] and kinds[0] is not None
 
646
                present_target = versioned[1] and kinds[1] is not None
 
647
 
 
648
                if kinds[0] is None and kinds[1] is None:
 
649
                    change_type = missing
 
650
                    display_path = real_path + marker
 
651
                elif present_source != present_target:
 
652
                    if present_target:
 
653
                        change_type = added
 
654
                    else:
 
655
                        assert present_source
 
656
                        change_type = removed
 
657
                    display_path = real_path + marker
 
658
                elif names[0] != names[1] or parent_ids[0] != parent_ids[1]:
 
659
                    # Renamed
 
660
                    if changed_content or executables[0] != executables[1]:
 
661
                        # and modified
 
662
                        change_type = renamed_and_modified
 
663
                    else:
 
664
                        change_type = renamed
 
665
                    display_path = (paths[0] + source_marker
 
666
                                    + ' => ' + paths[1] + marker)
 
667
                elif kinds[0] != kinds[1]:
 
668
                    change_type = kind_changed
 
669
                    display_path = (paths[0] + source_marker
 
670
                                    + ' => ' + paths[1] + marker)
 
671
                elif changed_content or executables[0] != executables[1]:
 
672
                    change_type = modified
 
673
                    display_path = real_path + marker
 
674
                else:
 
675
                    assert False, "How did we get here?"
 
676
 
 
677
                status.append((file_id, real_path, change_type, display_path))
 
678
        finally:
 
679
            source.unlock()
 
680
    finally:
 
681
        target.unlock()
 
682
 
 
683
    return status