/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: Curtis Hovey
  • Date: 2011-07-31 16:50:29 UTC
  • mto: This revision was merged to the branch mainline in revision 741.
  • Revision ID: sinzui.is@verizon.net-20110731165029-9gixuqypi3lwapzm
Removed import_pygtk because gi does not impicitly call Main(). Inlined checks for gtk availablility.

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