/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: Lucas Shrewsbury
  • Date: 2009-08-24 05:45:14 UTC
  • mto: This revision was merged to the branch mainline in revision 663.
  • Revision ID: rollbak@gmail.com-20090824054514-x5cteatkygzhw1ls
Fix #294632 by adding ignored emblem and correct status.
Fix #417966 by setting not emblem and correcting status.

* nautilus-bzr.py:
(BzrExtension.update_file_info): added check for ignored and unversioned files.

* setup.py: 
(data_files): added ignored emblem image.

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