/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: 2012-02-03 22:55:40 UTC
  • mto: This revision was merged to the branch mainline in revision 774.
  • Revision ID: sinzui.is@verizon.net-20120203225540-wg553pau7kjihy2k
Added a smoketest for bzr-handle-patch.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: UTF-8 -*-
2
1
"""Difference window.
3
2
 
4
3
This module contains the code to manage the diff window which shows
5
4
the changes made between two revisions on a branch.
6
5
"""
7
6
 
8
 
__copyright__ = "Copyright © 2005 Canonical Ltd."
9
 
__author__    = "Scott James Remnant <scott@ubuntu.com>"
 
7
__copyright__ = "Copyright 2005 Canonical Ltd."
 
8
__author__ = "Scott James Remnant <scott@ubuntu.com>"
10
9
 
11
10
 
12
11
from cStringIO import StringIO
13
12
 
14
 
import pygtk
15
 
pygtk.require("2.0")
16
 
import gtk
17
 
import pango
18
13
import os
19
14
import re
20
15
import sys
 
16
import inspect
 
17
try:
 
18
    from xml.etree.ElementTree import (
 
19
        Element,
 
20
        SubElement,
 
21
        tostring,
 
22
        )
 
23
    Element, SubElement, tostring  # Hush PEP8 redefinition.
 
24
except ImportError:
 
25
    from elementtree.ElementTree import (
 
26
        Element,
 
27
        SubElement,
 
28
        tostring,
 
29
        )
21
30
 
 
31
from gi.repository import Gtk
 
32
from gi.repository import Pango
22
33
try:
23
 
    import gtksourceview
 
34
    from gi.repository import GtkSource
24
35
    have_gtksourceview = True
25
36
except ImportError:
26
37
    have_gtksourceview = False
27
38
try:
28
 
    import gconf
 
39
    from gi.repository import GConf
29
40
    have_gconf = True
30
41
except ImportError:
31
42
    have_gconf = False
32
43
 
33
44
from bzrlib import (
 
45
    errors,
34
46
    merge as _mod_merge,
35
47
    osutils,
36
 
    progress,
37
48
    urlutils,
38
49
    workingtree,
39
 
)
40
 
from bzrlib.diff import show_diff_trees, internal_diff
41
 
from bzrlib.errors import NoSuchFile
 
50
    )
 
51
from bzrlib.diff import show_diff_trees
42
52
from bzrlib.patches import parse_patches
43
53
from bzrlib.trace import warning
44
 
from bzrlib.plugins.gtk import _i18n
 
54
from bzrlib.plugins.gtk.dialog import (
 
55
    error_dialog,
 
56
    info_dialog,
 
57
    warning_dialog,
 
58
    )
 
59
from bzrlib.plugins.gtk.i18n import _i18n
45
60
from bzrlib.plugins.gtk.window import Window
46
 
from dialog import error_dialog, info_dialog, warning_dialog
 
61
 
 
62
 
 
63
def fallback_guess_language(slm, content_type):
 
64
    for lang_id in slm.get_language_ids():
 
65
        lang = slm.get_language(lang_id)
 
66
        if "text/x-patch" in lang.get_mime_types():
 
67
            return lang
 
68
    return None
47
69
 
48
70
 
49
71
class SelectCancelled(Exception):
51
73
    pass
52
74
 
53
75
 
54
 
class DiffFileView(gtk.ScrolledWindow):
 
76
class DiffFileView(Gtk.ScrolledWindow):
55
77
    """Window for displaying diffs from a diff file"""
56
78
 
57
79
    def __init__(self):
58
 
        gtk.ScrolledWindow.__init__(self)
 
80
        super(DiffFileView, self).__init__()
59
81
        self.construct()
60
82
        self._diffs = {}
61
83
 
62
84
    def construct(self):
63
 
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
64
 
        self.set_shadow_type(gtk.SHADOW_IN)
 
85
        self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
 
86
        self.set_shadow_type(Gtk.ShadowType.IN)
65
87
 
66
88
        if have_gtksourceview:
67
 
            self.buffer = gtksourceview.SourceBuffer()
68
 
            slm = gtksourceview.SourceLanguagesManager()
69
 
            gsl = slm.get_language_from_mime_type("text/x-patch")
 
89
            self.buffer = GtkSource.Buffer()
 
90
            lang_manager = GtkSource.LanguageManager.get_default()
 
91
            language = lang_manager.guess_language(None, "text/x-patch")
70
92
            if have_gconf:
71
 
                self.apply_gedit_colors(gsl)
72
 
            self.apply_colordiff_colors(gsl)
73
 
            self.buffer.set_language(gsl)
74
 
            self.buffer.set_highlight(True)
 
93
                self.apply_gedit_colors(self.buffer)
 
94
            self.apply_colordiff_colors(self.buffer)
 
95
            self.buffer.set_language(language)
 
96
            self.buffer.set_highlight_syntax(True)
75
97
 
76
 
            sourceview = gtksourceview.SourceView(self.buffer)
 
98
            self.sourceview = GtkSource.View(buffer=self.buffer)
77
99
        else:
78
 
            self.buffer = gtk.TextBuffer()
79
 
            sourceview = gtk.TextView(self.buffer)
 
100
            self.buffer = Gtk.TextBuffer()
 
101
            self.sourceview = Gtk.TextView(self.buffer)
80
102
 
81
 
        sourceview.set_editable(False)
82
 
        sourceview.modify_font(pango.FontDescription("Monospace"))
83
 
        self.add(sourceview)
84
 
        sourceview.show()
 
103
        self.sourceview.set_editable(False)
 
104
        self.sourceview.modify_font(Pango.FontDescription("Monospace"))
 
105
        self.add(self.sourceview)
 
106
        self.sourceview.show()
85
107
 
86
108
    @staticmethod
87
 
    def apply_gedit_colors(lang):
88
 
        """Set style for lang to that specified in gedit configuration.
 
109
    def apply_gedit_colors(buf):
 
110
        """Set style to that specified in gedit configuration.
89
111
 
90
112
        This method needs the gconf module.
91
113
 
92
 
        :param lang: a gtksourceview.SourceLanguage object.
 
114
        :param buf: a GtkSource.Buffer object.
93
115
        """
94
 
        GEDIT_SYNTAX_PATH = '/apps/gedit-2/preferences/syntax_highlighting'
95
 
        GEDIT_LANG_PATH = GEDIT_SYNTAX_PATH + '/' + lang.get_id()
96
 
 
97
 
        client = gconf.client_get_default()
98
 
        client.add_dir(GEDIT_LANG_PATH, gconf.CLIENT_PRELOAD_NONE)
99
 
 
100
 
        for tag in lang.get_tags():
101
 
            tag_id = tag.get_id()
102
 
            gconf_key = GEDIT_LANG_PATH + '/' + tag_id
103
 
            style_string = client.get_string(gconf_key)
104
 
 
105
 
            if style_string is None:
106
 
                continue
107
 
 
108
 
            # function to get a bool from a string that's either '0' or '1'
109
 
            string_bool = lambda x: bool(int(x))
110
 
 
111
 
            # style_string is a string like "2/#FFCCAA/#000000/0/1/0/0"
112
 
            # values are: mask, fg, bg, italic, bold, underline, strike
113
 
            # this packs them into (str_value, attr_name, conv_func) tuples
114
 
            items = zip(style_string.split('/'), ['mask', 'foreground',
115
 
                'background', 'italic', 'bold', 'underline', 'strikethrough' ],
116
 
                [ int, gtk.gdk.color_parse, gtk.gdk.color_parse, string_bool,
117
 
                    string_bool, string_bool, string_bool ]
118
 
            )
119
 
 
120
 
            style = gtksourceview.SourceTagStyle()
121
 
 
122
 
            # XXX The mask attribute controls whether the present values of
123
 
            # foreground and background color should in fact be used. Ideally
124
 
            # (and that's what gedit does), one could set all three attributes,
125
 
            # and let the TagStyle object figure out which colors to use.
126
 
            # However, in the GtkSourceview python bindings, the mask attribute
127
 
            # is read-only, and it's derived instead from the colors being
128
 
            # set or not. This means that we have to sometimes refrain from
129
 
            # setting fg or bg colors, depending on the value of the mask.
130
 
            # This code could go away if mask were writable.
131
 
            mask = int(items[0][0])
132
 
            if not (mask & 1): # GTK_SOURCE_TAG_STYLE_USE_BACKGROUND
133
 
                items[2:3] = []
134
 
            if not (mask & 2): # GTK_SOURCE_TAG_STYLE_USE_FOREGROUND
135
 
                items[1:2] = []
136
 
            items[0:1] = [] # skip the mask unconditionally
137
 
 
138
 
            for value, attr, func in items:
139
 
                try:
140
 
                    value = func(value)
141
 
                except ValueError:
142
 
                    warning('gconf key %s contains an invalid value: %s'
143
 
                            % gconf_key, value)
144
 
                else:
145
 
                    setattr(style, attr, value)
146
 
 
147
 
            lang.set_tag_style(tag_id, style)
 
116
        GEDIT_SCHEME_PATH = '/apps/gedit-2/preferences/editor/colors/scheme'
 
117
        GEDIT_USER_STYLES_PATH = os.path.expanduser('~/.gnome2/gedit/styles')
 
118
 
 
119
        client = GConf.Client.get_default()
 
120
        style_scheme_name = client.get_string(GEDIT_SCHEME_PATH)
 
121
        if style_scheme_name is not None:
 
122
            style_scheme_mgr = GtkSource.StyleSchemeManager()
 
123
            style_scheme_mgr.append_search_path(GEDIT_USER_STYLES_PATH)
 
124
 
 
125
            style_scheme = style_scheme_mgr.get_scheme(style_scheme_name)
 
126
 
 
127
            if style_scheme is not None:
 
128
                buf.set_style_scheme(style_scheme)
148
129
 
149
130
    @classmethod
150
 
    def apply_colordiff_colors(klass, lang):
 
131
    def apply_colordiff_colors(klass, buf):
151
132
        """Set style colors for lang using the colordiff configuration file.
152
133
 
153
134
        Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
154
135
 
155
 
        :param lang: a "Diff" gtksourceview.SourceLanguage object.
 
136
        :param buf: a "Diff" GtkSource.Buffer object.
156
137
        """
157
 
        colors = {}
158
 
 
159
 
        for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
160
 
            f = os.path.expanduser(f)
161
 
            if os.path.exists(f):
162
 
                try:
163
 
                    f = file(f)
164
 
                except IOError, e:
165
 
                    warning('could not open file %s: %s' % (f, str(e)))
166
 
                else:
167
 
                    colors.update(klass.parse_colordiffrc(f))
168
 
                    f.close()
169
 
 
170
 
        if not colors:
171
 
            # ~/.colordiffrc does not exist
172
 
            return
173
 
 
174
 
        mapping = {
175
 
                # map GtkSourceView tags to colordiff names
 
138
        scheme_manager = GtkSource.StyleSchemeManager()
 
139
        style_scheme = scheme_manager.get_scheme('colordiff')
 
140
 
 
141
        # if style scheme not found, we'll generate it from colordiffrc
 
142
        # TODO: reload if colordiffrc has changed.
 
143
        if style_scheme is None:
 
144
            colors = {}
 
145
 
 
146
            for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
 
147
                f = os.path.expanduser(f)
 
148
                if os.path.exists(f):
 
149
                    try:
 
150
                        f = file(f)
 
151
                    except IOError, e:
 
152
                        warning('could not open file %s: %s' % (f, str(e)))
 
153
                    else:
 
154
                        colors.update(klass.parse_colordiffrc(f))
 
155
                        f.close()
 
156
 
 
157
            if not colors:
 
158
                # ~/.colordiffrc does not exist
 
159
                return
 
160
 
 
161
            mapping = {
 
162
                # map GtkSourceView2 scheme styles to colordiff names
176
163
                # since GSV is richer, accept new names for extra bits,
177
164
                # defaulting to old names if they're not present
178
 
                'Added@32@line': ['newtext'],
179
 
                'Removed@32@line': ['oldtext'],
180
 
                'Location': ['location', 'diffstuff'],
181
 
                'Diff@32@file': ['file', 'diffstuff'],
182
 
                'Special@32@case': ['specialcase', 'diffstuff'],
183
 
        }
184
 
 
185
 
        for tag in lang.get_tags():
186
 
            tag_id = tag.get_id()
187
 
            keys = mapping.get(tag_id, [])
188
 
            color = None
189
 
 
190
 
            for key in keys:
191
 
                color = colors.get(key, None)
192
 
                if color is not None:
193
 
                    break
194
 
 
195
 
            if color is None:
196
 
                continue
197
 
 
198
 
            style = gtksourceview.SourceTagStyle()
199
 
            try:
200
 
                style.foreground = gtk.gdk.color_parse(color)
201
 
            except ValueError:
202
 
                warning('not a valid color: %s' % color)
203
 
            else:
204
 
                lang.set_tag_style(tag_id, style)
 
165
                'diff:added-line': ['newtext'],
 
166
                'diff:removed-line': ['oldtext'],
 
167
                'diff:location': ['location', 'diffstuff'],
 
168
                'diff:file': ['file', 'diffstuff'],
 
169
                'diff:special-case': ['specialcase', 'diffstuff'],
 
170
            }
 
171
 
 
172
            converted_colors = {}
 
173
            for name, values in mapping.items():
 
174
                color = None
 
175
                for value in values:
 
176
                    color = colors.get(value, None)
 
177
                    if color is not None:
 
178
                        break
 
179
                if color is None:
 
180
                    continue
 
181
                converted_colors[name] = color
 
182
 
 
183
            # some xml magic to produce needed style scheme description
 
184
            e_style_scheme = Element('style-scheme')
 
185
            e_style_scheme.set('id', 'colordiff')
 
186
            e_style_scheme.set('_name', 'ColorDiff')
 
187
            e_style_scheme.set('version', '1.0')
 
188
            for name, color in converted_colors.items():
 
189
                style = SubElement(e_style_scheme, 'style')
 
190
                style.set('name', name)
 
191
                style.set('foreground', '#%s' % color)
 
192
 
 
193
            scheme_xml = tostring(e_style_scheme, 'UTF-8')
 
194
            if not os.path.exists(os.path.expanduser(
 
195
                '~/.local/share/gtksourceview-2.0/styles')):
 
196
                os.makedirs(os.path.expanduser(
 
197
                    '~/.local/share/gtksourceview-2.0/styles'))
 
198
            file(os.path.expanduser(
 
199
                '~/.local/share/gtksourceview-2.0/styles/colordiff.xml'),
 
200
                'w').write(scheme_xml)
 
201
 
 
202
            scheme_manager.force_rescan()
 
203
            style_scheme = scheme_manager.get_scheme('colordiff')
 
204
 
 
205
        buf.set_style_scheme(style_scheme)
205
206
 
206
207
    @staticmethod
207
208
    def parse_colordiffrc(fileobj):
228
229
#        self.parent_tree.lock_read()
229
230
#        self.rev_tree.lock_read()
230
231
#        try:
231
 
#            self.delta = iter_changes_to_status(self.parent_tree, self.rev_tree)
 
232
#            self.delta = iter_changes_to_status(
 
233
#               self.parent_tree, self.rev_tree)
232
234
#            self.path_to_status = {}
233
235
#            self.path_to_diff = {}
234
236
#            source_inv = self.parent_tree.inventory
235
237
#            target_inv = self.rev_tree.inventory
236
238
#            for (file_id, real_path, change_type, display_path) in self.delta:
237
 
#                self.path_to_status[real_path] = u'=== %s %s' % (change_type, display_path)
 
239
#                self.path_to_status[real_path] = u'=== %s %s' % (
 
240
#                    change_type, display_path)
238
241
#                if change_type in ('modified', 'renamed and modified'):
239
242
#                    source_ie = source_inv[file_id]
240
243
#                    target_ie = target_inv[file_id]
242
245
#                    source_ie.diff(internal_diff, *old path, *old_tree,
243
246
#                                   *new_path, target_ie, self.rev_tree,
244
247
#                                   sio)
245
 
#                    self.path_to_diff[real_path] = 
 
248
#                    self.path_to_diff[real_path] =
246
249
#
247
250
#        finally:
248
251
#            self.rev_tree.unlock()
262
265
    """This is the soft and chewy filling for a DiffWindow."""
263
266
 
264
267
    def __init__(self):
265
 
        DiffFileView.__init__(self)
 
268
        super(DiffView, self).__init__()
266
269
        self.rev_tree = None
267
270
        self.parent_tree = None
268
271
 
292
295
        self.buffer.set_text(decoded.encode('UTF-8'))
293
296
 
294
297
 
295
 
class DiffWidget(gtk.HPaned):
 
298
class DiffWidget(Gtk.HPaned):
296
299
    """Diff widget
297
300
 
298
301
    """
 
302
 
 
303
    SHOW_WIDGETS = True
 
304
 
299
305
    def __init__(self):
300
306
        super(DiffWidget, self).__init__()
301
307
 
302
308
        # The file hierarchy: a scrollable treeview
303
 
        scrollwin = gtk.ScrolledWindow()
304
 
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
305
 
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
 
309
        scrollwin = Gtk.ScrolledWindow()
 
310
        scrollwin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
 
311
        scrollwin.set_shadow_type(Gtk.ShadowType.IN)
306
312
        self.pack1(scrollwin)
307
 
        scrollwin.show()
 
313
        if self.SHOW_WIDGETS:
 
314
            scrollwin.show()
308
315
 
309
 
        self.model = gtk.TreeStore(str, str)
310
 
        self.treeview = gtk.TreeView(self.model)
 
316
        self.model = Gtk.TreeStore(str, str)
 
317
        self.treeview = Gtk.TreeView(model=self.model)
311
318
        self.treeview.set_headers_visible(False)
312
319
        self.treeview.set_search_column(1)
313
320
        self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
314
321
        scrollwin.add(self.treeview)
315
 
        self.treeview.show()
 
322
        if self.SHOW_WIDGETS:
 
323
            self.treeview.show()
316
324
 
317
 
        cell = gtk.CellRendererText()
 
325
        cell = Gtk.CellRendererText()
318
326
        cell.set_property("width-chars", 20)
319
 
        column = gtk.TreeViewColumn()
320
 
        column.pack_start(cell, expand=True)
 
327
        column = Gtk.TreeViewColumn()
 
328
        column.pack_start(cell, True)
321
329
        column.add_attribute(cell, "text", 0)
322
330
        self.treeview.append_column(column)
323
331
 
330
338
        # text view
331
339
 
332
340
    def set_diff_text_sections(self, sections):
333
 
        self.diff_view = DiffFileView()
334
 
        self.diff_view.show()
335
 
        self.pack2(self.diff_view)
 
341
        if getattr(self, 'diff_view', None) is None:
 
342
            self.diff_view = DiffFileView()
 
343
            self.pack2(self.diff_view)
 
344
        if self.SHOW_WIDGETS:
 
345
            self.diff_view.show()
336
346
        for oldname, newname, patch in sections:
337
347
            self.diff_view._diffs[newname] = str(patch)
338
348
            if newname is None:
346
356
        Compares the two trees and populates the window with the
347
357
        differences.
348
358
        """
349
 
        self.diff_view = DiffView()
350
 
        self.pack2(self.diff_view)
351
 
        self.diff_view.show()
 
359
        if getattr(self, 'diff_view', None) is None:
 
360
            self.diff_view = DiffView()
 
361
            self.pack2(self.diff_view)
 
362
        if self.SHOW_WIDGETS:
 
363
            self.diff_view.show()
352
364
        self.diff_view.set_trees(rev_tree, parent_tree)
353
365
        self.rev_tree = rev_tree
354
366
        self.parent_tree = parent_tree
356
368
        self.model.clear()
357
369
        delta = self.rev_tree.changes_from(self.parent_tree)
358
370
 
359
 
        self.model.append(None, [ "Complete Diff", "" ])
 
371
        self.model.append(None, ["Complete Diff", ""])
360
372
 
361
373
        if len(delta.added):
362
 
            titer = self.model.append(None, [ "Added", None ])
 
374
            titer = self.model.append(None, ["Added", None])
363
375
            for path, id, kind in delta.added:
364
 
                self.model.append(titer, [ path, path ])
 
376
                self.model.append(titer, [path, path])
365
377
 
366
378
        if len(delta.removed):
367
 
            titer = self.model.append(None, [ "Removed", None ])
 
379
            titer = self.model.append(None, ["Removed", None])
368
380
            for path, id, kind in delta.removed:
369
 
                self.model.append(titer, [ path, path ])
 
381
                self.model.append(titer, [path, path])
370
382
 
371
383
        if len(delta.renamed):
372
 
            titer = self.model.append(None, [ "Renamed", None ])
 
384
            titer = self.model.append(None, ["Renamed", None])
373
385
            for oldpath, newpath, id, kind, text_modified, meta_modified \
374
386
                    in delta.renamed:
375
 
                self.model.append(titer, [ oldpath, newpath ])
 
387
                self.model.append(titer, [oldpath, newpath])
376
388
 
377
389
        if len(delta.modified):
378
 
            titer = self.model.append(None, [ "Modified", None ])
 
390
            titer = self.model.append(None, ["Modified", None])
379
391
            for path, id, kind, text_modified, meta_modified in delta.modified:
380
 
                self.model.append(titer, [ path, path ])
 
392
                self.model.append(titer, [path, path])
381
393
 
382
394
        self.treeview.expand_all()
 
395
        self.diff_view.show_diff(None)
383
396
 
384
397
    def set_file(self, file_path):
385
398
        """Select the current file to display"""
390
403
                    tv_path = child.path
391
404
                    break
392
405
        if tv_path is None:
393
 
            raise NoSuchFile(file_path)
394
 
        self.treeview.set_cursor(tv_path)
 
406
            raise errors.NoSuchFile(file_path)
 
407
        self.treeview.set_cursor(tv_path, None, False)
395
408
        self.treeview.scroll_to_cell(tv_path)
396
409
 
397
410
    def _treeview_cursor_cb(self, *args):
398
411
        """Callback for when the treeview cursor changes."""
399
412
        (path, col) = self.treeview.get_cursor()
400
 
        specific_files = [ self.model[path][1] ]
401
 
        if specific_files == [ None ]:
402
 
            return
403
 
        elif specific_files == [ "" ]:
 
413
        if path is None:
 
414
            return
 
415
        specific_files = [self.model[path][1]]
 
416
        if specific_files == [None]:
 
417
            return
 
418
        elif specific_files == [""]:
404
419
            specific_files = None
405
420
 
406
421
        self.diff_view.show_diff(specific_files)
407
422
 
 
423
    def _on_wraplines_toggled(self, widget=None, wrap=False):
 
424
        """Callback for when the wrap lines checkbutton is toggled"""
 
425
        if wrap or widget.get_active():
 
426
            self.diff_view.sourceview.set_wrap_mode(Gtk.WrapMode.WORD)
 
427
        else:
 
428
            self.diff_view.sourceview.set_wrap_mode(Gtk.WrapMode.NONE)
 
429
 
408
430
 
409
431
class DiffWindow(Window):
410
432
    """Diff window.
413
435
    differences between two revisions on a branch.
414
436
    """
415
437
 
 
438
    SHOW_WIDGETS = True
 
439
 
416
440
    def __init__(self, parent=None, operations=None):
417
 
        Window.__init__(self, parent)
 
441
        super(DiffWindow, self).__init__(parent=parent)
418
442
        self.set_border_width(0)
419
 
        self.set_title("bzrk diff")
 
443
        self.set_title("bzr diff")
420
444
 
421
445
        # Use two thirds of the screen by default
422
446
        screen = self.get_screen()
428
452
 
429
453
    def construct(self, operations):
430
454
        """Construct the window contents."""
431
 
        self.vbox = gtk.VBox()
 
455
        self.vbox = Gtk.VBox()
432
456
        self.add(self.vbox)
433
 
        self.vbox.show()
 
457
        if self.SHOW_WIDGETS:
 
458
            self.vbox.show()
 
459
        self.diff = DiffWidget()
 
460
        self.vbox.pack_end(self.diff, True, True, 0)
 
461
        if self.SHOW_WIDGETS:
 
462
            self.diff.show_all()
 
463
        # Build after DiffWidget to connect signals
 
464
        menubar = self._get_menu_bar()
 
465
        self.vbox.pack_start(menubar, False, False, 0)
434
466
        hbox = self._get_button_bar(operations)
435
467
        if hbox is not None:
436
 
            self.vbox.pack_start(hbox, expand=False, fill=True)
437
 
        self.diff = DiffWidget()
438
 
        self.vbox.add(self.diff)
439
 
        self.diff.show_all()
 
468
            self.vbox.pack_start(hbox, False, True, 0)
 
469
 
 
470
    def _get_menu_bar(self):
 
471
        menubar = Gtk.MenuBar()
 
472
        # View menu
 
473
        mb_view = Gtk.MenuItem.new_with_mnemonic(_i18n("_View"))
 
474
        mb_view_menu = Gtk.Menu()
 
475
        mb_view_wrapsource = Gtk.CheckMenuItem.new_with_mnemonic(
 
476
            _i18n("Wrap _Long Lines"))
 
477
        mb_view_wrapsource.connect('activate', self.diff._on_wraplines_toggled)
 
478
        mb_view_menu.append(mb_view_wrapsource)
 
479
        mb_view.set_submenu(mb_view_menu)
 
480
        menubar.append(mb_view)
 
481
        if self.SHOW_WIDGETS:
 
482
            menubar.show_all()
 
483
        return menubar
440
484
 
441
485
    def _get_button_bar(self, operations):
442
486
        """Return a button bar to use.
445
489
        """
446
490
        if operations is None:
447
491
            return None
448
 
        hbox = gtk.HButtonBox()
449
 
        hbox.set_layout(gtk.BUTTONBOX_START)
 
492
        hbox = Gtk.HButtonBox()
 
493
        hbox.set_layout(Gtk.ButtonBoxStyle.START)
450
494
        for title, method in operations:
451
 
            merge_button = gtk.Button(title)
452
 
            merge_button.show()
453
 
            merge_button.set_relief(gtk.RELIEF_NONE)
 
495
            merge_button = Gtk.Button(title)
 
496
            if self.SHOW_WIDGETS:
 
497
                merge_button.show()
 
498
            merge_button.set_relief(Gtk.ReliefStyle.NONE)
454
499
            merge_button.connect("clicked", method)
455
 
            hbox.pack_start(merge_button, expand=False, fill=True)
456
 
        hbox.show()
 
500
            hbox.pack_start(merge_button, False, True, 0)
 
501
        if self.SHOW_WIDGETS:
 
502
            hbox.show()
457
503
        return hbox
458
504
 
459
505
    def _get_merge_target(self):
460
 
        d = gtk.FileChooserDialog('Merge branch', self,
461
 
                                  gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
462
 
                                  buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
463
 
                                           gtk.STOCK_CANCEL,
464
 
                                           gtk.RESPONSE_CANCEL,))
 
506
        d = Gtk.FileChooserDialog('Merge branch', self,
 
507
                                  Gtk.FileChooserAction.SELECT_FOLDER,
 
508
                                  buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK,
 
509
                                           Gtk.STOCK_CANCEL,
 
510
                                           Gtk.ResponseType.CANCEL,))
465
511
        try:
466
512
            result = d.run()
467
 
            if result != gtk.RESPONSE_OK:
 
513
            if result != Gtk.ResponseType.OK:
468
514
                raise SelectCancelled()
469
515
            return d.get_current_folder_uri()
470
516
        finally:
484
530
        error_dialog('Error', str(e))
485
531
 
486
532
    def _get_save_path(self, basename):
487
 
        d = gtk.FileChooserDialog('Save As', self,
488
 
                                  gtk.FILE_CHOOSER_ACTION_SAVE,
489
 
                                  buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
490
 
                                           gtk.STOCK_CANCEL,
491
 
                                           gtk.RESPONSE_CANCEL,))
 
533
        d = Gtk.FileChooserDialog('Save As', self,
 
534
                                  Gtk.FileChooserAction.SAVE,
 
535
                                  buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK,
 
536
                                           Gtk.STOCK_CANCEL,
 
537
                                           Gtk.ResponseType.CANCEL,))
492
538
        d.set_current_name(basename)
493
539
        try:
494
540
            result = d.run()
495
 
            if result != gtk.RESPONSE_OK:
 
541
            if result != Gtk.ResponseType.OK:
496
542
                raise SelectCancelled()
497
543
            return urlutils.local_path_from_url(d.get_uri())
498
544
        finally:
513
559
 
514
560
class DiffController(object):
515
561
 
516
 
    def __init__(self, path, patch, window=None):
 
562
    def __init__(self, path, patch, window=None, allow_dirty=False):
517
563
        self.path = path
518
564
        self.patch = patch
 
565
        self.allow_dirty = allow_dirty
519
566
        if window is None:
520
567
            window = DiffWindow(operations=self._provide_operations())
521
568
            self.initialize_window(window)
527
574
 
528
575
    def get_diff_sections(self):
529
576
        yield "Complete Diff", None, ''.join(self.patch)
530
 
        for patch in parse_patches(self.patch):
 
577
        # allow_dirty was added to parse_patches in bzrlib 2.2b1
 
578
        if 'allow_dirty' in inspect.getargspec(parse_patches).args:
 
579
            patches = parse_patches(self.patch, allow_dirty=self.allow_dirty)
 
580
        else:
 
581
            patches = parse_patches(self.patch)
 
582
        for patch in patches:
531
583
            oldname = patch.oldname.split('\t')[0]
532
584
            newname = patch.newname.split('\t')[0]
533
585
            yield oldname, newname, str(patch)
554
606
class MergeDirectiveController(DiffController):
555
607
 
556
608
    def __init__(self, path, directive, window=None):
557
 
        DiffController.__init__(self, path, directive.patch.splitlines(True),
558
 
                                window)
 
609
        super(MergeDirectiveController, self).__init__(
 
610
            path, directive.patch.splitlines(True), window)
559
611
        self.directive = directive
560
612
        self.merge_target = None
561
613
 
572
624
        tree.lock_write()
573
625
        try:
574
626
            try:
575
 
                merger, verified = _mod_merge.Merger.from_mergeable(tree,
576
 
                    self.directive, progress.DummyProgress())
577
 
                merger.check_basis(True)
 
627
                if tree.has_changes():
 
628
                    raise errors.UncommittedChanges(tree)
 
629
                merger, verified = _mod_merge.Merger.from_mergeable(
 
630
                    tree, self.directive, pb=None)
578
631
                merger.merge_type = _mod_merge.Merge3Merger
579
632
                conflict_count = merger.do_merge()
580
633
                merger.set_pending()
606
659
    renamed_and_modified = 'renamed and modified'
607
660
    modified = 'modified'
608
661
    kind_changed = 'kind changed'
 
662
    missing = 'missing'
609
663
 
610
664
    # TODO: Handle metadata changes
611
665
 
626
680
                    source_marker = ''
627
681
                else:
628
682
                    source_marker = osutils.kind_marker(kinds[0])
 
683
 
629
684
                if kinds[1] is None:
630
 
                    assert kinds[0] is not None
631
 
                    marker = osutils.kind_marker(kinds[0])
 
685
                    if kinds[0] is None:
 
686
                        # We assume bzr will flag only files in that case,
 
687
                        # there may be a bzr bug there as only files seems to
 
688
                        # not receive any kind.
 
689
                        marker = osutils.kind_marker('file')
 
690
                    else:
 
691
                        marker = osutils.kind_marker(kinds[0])
632
692
                else:
633
693
                    marker = osutils.kind_marker(kinds[1])
634
694
 
636
696
                if real_path is None:
637
697
                    real_path = paths[0]
638
698
                assert real_path is not None
639
 
                display_path = real_path + marker
640
699
 
641
700
                present_source = versioned[0] and kinds[0] is not None
642
701
                present_target = versioned[1] and kinds[1] is not None
643
702
 
644
 
                if present_source != present_target:
 
703
                if kinds[0] is None and kinds[1] is None:
 
704
                    change_type = missing
 
705
                    display_path = real_path + marker
 
706
                elif present_source != present_target:
645
707
                    if present_target:
646
708
                        change_type = added
647
709
                    else:
648
710
                        assert present_source
649
711
                        change_type = removed
 
712
                    display_path = real_path + marker
650
713
                elif names[0] != names[1] or parent_ids[0] != parent_ids[1]:
651
714
                    # Renamed
652
715
                    if changed_content or executables[0] != executables[1]:
660
723
                    change_type = kind_changed
661
724
                    display_path = (paths[0] + source_marker
662
725
                                    + ' => ' + paths[1] + marker)
663
 
                elif changed_content is True or executables[0] != executables[1]:
 
726
                elif changed_content or executables[0] != executables[1]:
664
727
                    change_type = modified
 
728
                    display_path = real_path + marker
665
729
                else:
666
730
                    assert False, "How did we get here?"
667
731