/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: David Planella
  • Date: 2011-03-06 08:24:07 UTC
  • mfrom: (718 trunk)
  • mto: This revision was merged to the branch mainline in revision 719.
  • Revision ID: david.planella@ubuntu.com-20110306082407-y9zwkjje5oue9egw
Added preliminary internationalization support. Merged from trunk.

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