/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: Mark Lee
  • Date: 2009-07-11 18:39:14 UTC
  • mto: This revision was merged to the branch mainline in revision 661.
  • Revision ID: bzr@lazymalevolence.com-20090711183914-zuii3et5skiv2njo
Re-ignore credits.pickle.

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