/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: Andrew Starr-Bochicchio
  • Author(s): Mathias Brodala
  • Date: 2010-01-25 01:58:58 UTC
  • mto: This revision was merged to the branch mainline in revision 673.
  • Revision ID: a.starr.b@gmail.com-20100125015858-tphqkvgyfrvob63o
olive/menu.py: Correctly import commit dialog.

Show diffs side-by-side

added added

removed removed

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