/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: Jelmer Vernooij
  • Date: 2012-07-09 15:23:26 UTC
  • mto: This revision was merged to the branch mainline in revision 794.
  • Revision ID: jelmer@samba.org-20120709152326-dzxb8zoz0btull7n
Remove bzr-notify.

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
 
import os
19
 
import re
20
13
import sys
 
14
import inspect
21
15
 
 
16
from gi.repository import Gtk
 
17
from gi.repository import Pango
22
18
try:
23
 
    import gtksourceview
 
19
    from gi.repository import GtkSource
24
20
    have_gtksourceview = True
25
21
except ImportError:
26
22
    have_gtksourceview = False
27
 
try:
28
 
    import gconf
29
 
    have_gconf = True
30
 
except ImportError:
31
 
    have_gconf = False
32
23
 
33
 
from bzrlib import merge as _mod_merge, osutils, progress, workingtree
34
 
from bzrlib.diff import show_diff_trees, internal_diff
35
 
from bzrlib.errors import NoSuchFile
 
24
from bzrlib import (
 
25
    errors,
 
26
    merge as _mod_merge,
 
27
    osutils,
 
28
    urlutils,
 
29
    workingtree,
 
30
    )
 
31
from bzrlib.diff import show_diff_trees
36
32
from bzrlib.patches import parse_patches
37
 
from bzrlib.trace import warning
 
33
from bzrlib.plugins.gtk.dialog import (
 
34
    error_dialog,
 
35
    info_dialog,
 
36
    warning_dialog,
 
37
    )
 
38
from bzrlib.plugins.gtk.i18n import _i18n
38
39
from bzrlib.plugins.gtk.window import Window
39
 
from dialog import error_dialog, info_dialog, warning_dialog
 
40
 
 
41
 
 
42
def fallback_guess_language(slm, content_type):
 
43
    for lang_id in slm.get_language_ids():
 
44
        lang = slm.get_language(lang_id)
 
45
        if "text/x-patch" in lang.get_mime_types():
 
46
            return lang
 
47
    return None
40
48
 
41
49
 
42
50
class SelectCancelled(Exception):
44
52
    pass
45
53
 
46
54
 
47
 
class DiffFileView(gtk.ScrolledWindow):
 
55
class DiffFileView(Gtk.ScrolledWindow):
 
56
    """Window for displaying diffs from a diff file"""
 
57
 
 
58
    SHOW_WIDGETS = True
48
59
 
49
60
    def __init__(self):
50
 
        gtk.ScrolledWindow.__init__(self)
 
61
        super(DiffFileView, self).__init__()
51
62
        self.construct()
52
63
        self._diffs = {}
53
64
 
54
65
    def construct(self):
55
 
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
56
 
        self.set_shadow_type(gtk.SHADOW_IN)
 
66
        self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
 
67
        self.set_shadow_type(Gtk.ShadowType.IN)
57
68
 
58
69
        if have_gtksourceview:
59
 
            self.buffer = gtksourceview.SourceBuffer()
60
 
            slm = gtksourceview.SourceLanguagesManager()
61
 
            gsl = slm.get_language_from_mime_type("text/x-patch")
62
 
            if have_gconf:
63
 
                self.apply_gedit_colors(gsl)
64
 
            self.apply_colordiff_colors(gsl)
65
 
            self.buffer.set_language(gsl)
66
 
            self.buffer.set_highlight(True)
67
 
 
68
 
            sourceview = gtksourceview.SourceView(self.buffer)
 
70
            self.buffer = GtkSource.Buffer()
 
71
            lang_manager = GtkSource.LanguageManager.get_default()
 
72
            language = lang_manager.guess_language(None, "text/x-patch")
 
73
            self.buffer.set_language(language)
 
74
            self.buffer.set_highlight_syntax(True)
 
75
            self.sourceview = GtkSource.View(buffer=self.buffer)
69
76
        else:
70
 
            self.buffer = gtk.TextBuffer()
71
 
            sourceview = gtk.TextView(self.buffer)
72
 
 
73
 
        sourceview.set_editable(False)
74
 
        sourceview.modify_font(pango.FontDescription("Monospace"))
75
 
        self.add(sourceview)
76
 
        sourceview.show()
77
 
 
78
 
    @staticmethod
79
 
    def apply_gedit_colors(lang):
80
 
        """Set style for lang to that specified in gedit configuration.
81
 
 
82
 
        This method needs the gconf module.
83
 
 
84
 
        :param lang: a gtksourceview.SourceLanguage object.
85
 
        """
86
 
        GEDIT_SYNTAX_PATH = '/apps/gedit-2/preferences/syntax_highlighting'
87
 
        GEDIT_LANG_PATH = GEDIT_SYNTAX_PATH + '/' + lang.get_id()
88
 
 
89
 
        client = gconf.client_get_default()
90
 
        client.add_dir(GEDIT_LANG_PATH, gconf.CLIENT_PRELOAD_NONE)
91
 
 
92
 
        for tag in lang.get_tags():
93
 
            tag_id = tag.get_id()
94
 
            gconf_key = GEDIT_LANG_PATH + '/' + tag_id
95
 
            style_string = client.get_string(gconf_key)
96
 
 
97
 
            if style_string is None:
98
 
                continue
99
 
 
100
 
            # function to get a bool from a string that's either '0' or '1'
101
 
            string_bool = lambda x: bool(int(x))
102
 
 
103
 
            # style_string is a string like "2/#FFCCAA/#000000/0/1/0/0"
104
 
            # values are: mask, fg, bg, italic, bold, underline, strike
105
 
            # this packs them into (str_value, attr_name, conv_func) tuples
106
 
            items = zip(style_string.split('/'), ['mask', 'foreground',
107
 
                'background', 'italic', 'bold', 'underline', 'strikethrough' ],
108
 
                [ int, gtk.gdk.color_parse, gtk.gdk.color_parse, string_bool,
109
 
                    string_bool, string_bool, string_bool ]
110
 
            )
111
 
 
112
 
            style = gtksourceview.SourceTagStyle()
113
 
 
114
 
            # XXX The mask attribute controls whether the present values of
115
 
            # foreground and background color should in fact be used. Ideally
116
 
            # (and that's what gedit does), one could set all three attributes,
117
 
            # and let the TagStyle object figure out which colors to use.
118
 
            # However, in the GtkSourceview python bindings, the mask attribute
119
 
            # is read-only, and it's derived instead from the colors being
120
 
            # set or not. This means that we have to sometimes refrain from
121
 
            # setting fg or bg colors, depending on the value of the mask.
122
 
            # This code could go away if mask were writable.
123
 
            mask = int(items[0][0])
124
 
            if not (mask & 1): # GTK_SOURCE_TAG_STYLE_USE_BACKGROUND
125
 
                items[2:3] = []
126
 
            if not (mask & 2): # GTK_SOURCE_TAG_STYLE_USE_FOREGROUND
127
 
                items[1:2] = []
128
 
            items[0:1] = [] # skip the mask unconditionally
129
 
 
130
 
            for value, attr, func in items:
131
 
                try:
132
 
                    value = func(value)
133
 
                except ValueError:
134
 
                    warning('gconf key %s contains an invalid value: %s'
135
 
                            % gconf_key, value)
136
 
                else:
137
 
                    setattr(style, attr, value)
138
 
 
139
 
            lang.set_tag_style(tag_id, style)
140
 
 
141
 
    @classmethod
142
 
    def apply_colordiff_colors(klass, lang):
143
 
        """Set style colors for lang using the colordiff configuration file.
144
 
 
145
 
        Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
146
 
 
147
 
        :param lang: a "Diff" gtksourceview.SourceLanguage object.
148
 
        """
149
 
        colors = {}
150
 
 
151
 
        for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
152
 
            f = os.path.expanduser(f)
153
 
            if os.path.exists(f):
154
 
                try:
155
 
                    f = file(f)
156
 
                except IOError, e:
157
 
                    warning('could not open file %s: %s' % (f, str(e)))
158
 
                else:
159
 
                    colors.update(klass.parse_colordiffrc(f))
160
 
                    f.close()
161
 
 
162
 
        if not colors:
163
 
            # ~/.colordiffrc does not exist
164
 
            return
165
 
 
166
 
        mapping = {
167
 
                # map GtkSourceView tags to colordiff names
168
 
                # since GSV is richer, accept new names for extra bits,
169
 
                # defaulting to old names if they're not present
170
 
                'Added@32@line': ['newtext'],
171
 
                'Removed@32@line': ['oldtext'],
172
 
                'Location': ['location', 'diffstuff'],
173
 
                'Diff@32@file': ['file', 'diffstuff'],
174
 
                'Special@32@case': ['specialcase', 'diffstuff'],
175
 
        }
176
 
 
177
 
        for tag in lang.get_tags():
178
 
            tag_id = tag.get_id()
179
 
            keys = mapping.get(tag_id, [])
180
 
            color = None
181
 
 
182
 
            for key in keys:
183
 
                color = colors.get(key, None)
184
 
                if color is not None:
185
 
                    break
186
 
 
187
 
            if color is None:
188
 
                continue
189
 
 
190
 
            style = gtksourceview.SourceTagStyle()
191
 
            try:
192
 
                style.foreground = gtk.gdk.color_parse(color)
193
 
            except ValueError:
194
 
                warning('not a valid color: %s' % color)
195
 
            else:
196
 
                lang.set_tag_style(tag_id, style)
197
 
 
198
 
    @staticmethod
199
 
    def parse_colordiffrc(fileobj):
200
 
        """Parse fileobj as a colordiff configuration file.
201
 
 
202
 
        :return: A dict with the key -> value pairs.
203
 
        """
204
 
        colors = {}
205
 
        for line in fileobj:
206
 
            if re.match(r'^\s*#', line):
207
 
                continue
208
 
            if '=' not in line:
209
 
                continue
210
 
            key, val = line.split('=', 1)
211
 
            colors[key.strip()] = val.strip()
212
 
        return colors
 
77
            self.buffer = Gtk.TextBuffer()
 
78
            self.sourceview = Gtk.TextView(self.buffer)
 
79
 
 
80
        self.sourceview.set_editable(False)
 
81
        self.sourceview.override_font(Pango.FontDescription("Monospace"))
 
82
        self.add(self.sourceview)
 
83
        if self.SHOW_WIDGETS:
 
84
            self.sourceview.show()
213
85
 
214
86
    def set_trees(self, rev_tree, parent_tree):
215
87
        self.rev_tree = rev_tree
220
92
#        self.parent_tree.lock_read()
221
93
#        self.rev_tree.lock_read()
222
94
#        try:
223
 
#            self.delta = _iter_changes_to_status(self.parent_tree, self.rev_tree)
 
95
#            self.delta = iter_changes_to_status(
 
96
#               self.parent_tree, self.rev_tree)
224
97
#            self.path_to_status = {}
225
98
#            self.path_to_diff = {}
226
99
#            source_inv = self.parent_tree.inventory
227
100
#            target_inv = self.rev_tree.inventory
228
101
#            for (file_id, real_path, change_type, display_path) in self.delta:
229
 
#                self.path_to_status[real_path] = u'=== %s %s' % (change_type, display_path)
 
102
#                self.path_to_status[real_path] = u'=== %s %s' % (
 
103
#                    change_type, display_path)
230
104
#                if change_type in ('modified', 'renamed and modified'):
231
105
#                    source_ie = source_inv[file_id]
232
106
#                    target_ie = target_inv[file_id]
234
108
#                    source_ie.diff(internal_diff, *old path, *old_tree,
235
109
#                                   *new_path, target_ie, self.rev_tree,
236
110
#                                   sio)
237
 
#                    self.path_to_diff[real_path] = 
 
111
#                    self.path_to_diff[real_path] =
238
112
#
239
113
#        finally:
240
114
#            self.rev_tree.unlock()
254
128
    """This is the soft and chewy filling for a DiffWindow."""
255
129
 
256
130
    def __init__(self):
257
 
        DiffFileView.__init__(self)
 
131
        super(DiffView, self).__init__()
258
132
        self.rev_tree = None
259
133
        self.parent_tree = None
260
134
 
261
135
    def show_diff(self, specific_files):
 
136
        """Show the diff for the specified files"""
262
137
        s = StringIO()
263
138
        show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files,
264
139
                        old_label='', new_label='',
283
158
        self.buffer.set_text(decoded.encode('UTF-8'))
284
159
 
285
160
 
286
 
class DiffWidget(gtk.HPaned):
 
161
class DiffWidget(Gtk.HPaned):
287
162
    """Diff widget
288
163
 
289
164
    """
 
165
 
 
166
    SHOW_WIDGETS = True
 
167
 
290
168
    def __init__(self):
291
169
        super(DiffWidget, self).__init__()
292
170
 
293
171
        # The file hierarchy: a scrollable treeview
294
 
        scrollwin = gtk.ScrolledWindow()
295
 
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
296
 
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
 
172
        scrollwin = Gtk.ScrolledWindow()
 
173
        scrollwin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
 
174
        scrollwin.set_shadow_type(Gtk.ShadowType.IN)
297
175
        self.pack1(scrollwin)
298
 
        scrollwin.show()
 
176
        if self.SHOW_WIDGETS:
 
177
            scrollwin.show()
299
178
 
300
 
        self.model = gtk.TreeStore(str, str)
301
 
        self.treeview = gtk.TreeView(self.model)
 
179
        self.model = Gtk.TreeStore(str, str)
 
180
        self.treeview = Gtk.TreeView(model=self.model)
302
181
        self.treeview.set_headers_visible(False)
303
182
        self.treeview.set_search_column(1)
304
183
        self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
305
184
        scrollwin.add(self.treeview)
306
 
        self.treeview.show()
 
185
        if self.SHOW_WIDGETS:
 
186
            self.treeview.show()
307
187
 
308
 
        cell = gtk.CellRendererText()
 
188
        cell = Gtk.CellRendererText()
309
189
        cell.set_property("width-chars", 20)
310
 
        column = gtk.TreeViewColumn()
311
 
        column.pack_start(cell, expand=True)
 
190
        column = Gtk.TreeViewColumn()
 
191
        column.pack_start(cell, True)
312
192
        column.add_attribute(cell, "text", 0)
313
193
        self.treeview.append_column(column)
314
194
 
315
195
    def set_diff_text(self, lines):
 
196
        """Set the current diff from a list of lines
 
197
 
 
198
        :param lines: The diff to show, in unified diff format
 
199
        """
316
200
        # The diffs of the  selected file: a scrollable source or
317
201
        # text view
318
 
        self.diff_view = DiffFileView()
319
 
        self.diff_view.show()
320
 
        self.pack2(self.diff_view)
321
 
        self.model.append(None, [ "Complete Diff", "" ])
322
 
        self.diff_view._diffs[None] = ''.join(lines)
323
 
        for patch in parse_patches(lines):
324
 
            oldname = patch.oldname.split('\t')[0]
325
 
            newname = patch.newname.split('\t')[0]
 
202
 
 
203
    def set_diff_text_sections(self, sections):
 
204
        if getattr(self, 'diff_view', None) is None:
 
205
            self.diff_view = DiffFileView()
 
206
            self.pack2(self.diff_view)
 
207
        if self.SHOW_WIDGETS:
 
208
            self.diff_view.show()
 
209
        for oldname, newname, patch in sections:
 
210
            self.diff_view._diffs[newname] = str(patch)
 
211
            if newname is None:
 
212
                newname = ''
326
213
            self.model.append(None, [oldname, newname])
327
 
            self.diff_view._diffs[newname] = str(patch)
328
214
        self.diff_view.show_diff(None)
329
215
 
330
216
    def set_diff(self, rev_tree, parent_tree):
333
219
        Compares the two trees and populates the window with the
334
220
        differences.
335
221
        """
336
 
        self.diff_view = DiffView()
337
 
        self.pack2(self.diff_view)
338
 
        self.diff_view.show()
 
222
        if getattr(self, 'diff_view', None) is None:
 
223
            self.diff_view = DiffView()
 
224
            self.pack2(self.diff_view)
 
225
        if self.SHOW_WIDGETS:
 
226
            self.diff_view.show()
339
227
        self.diff_view.set_trees(rev_tree, parent_tree)
340
228
        self.rev_tree = rev_tree
341
229
        self.parent_tree = parent_tree
343
231
        self.model.clear()
344
232
        delta = self.rev_tree.changes_from(self.parent_tree)
345
233
 
346
 
        self.model.append(None, [ "Complete Diff", "" ])
 
234
        self.model.append(None, ["Complete Diff", ""])
347
235
 
348
236
        if len(delta.added):
349
 
            titer = self.model.append(None, [ "Added", None ])
 
237
            titer = self.model.append(None, ["Added", None])
350
238
            for path, id, kind in delta.added:
351
 
                self.model.append(titer, [ path, path ])
 
239
                self.model.append(titer, [path, path])
352
240
 
353
241
        if len(delta.removed):
354
 
            titer = self.model.append(None, [ "Removed", None ])
 
242
            titer = self.model.append(None, ["Removed", None])
355
243
            for path, id, kind in delta.removed:
356
 
                self.model.append(titer, [ path, path ])
 
244
                self.model.append(titer, [path, path])
357
245
 
358
246
        if len(delta.renamed):
359
 
            titer = self.model.append(None, [ "Renamed", None ])
 
247
            titer = self.model.append(None, ["Renamed", None])
360
248
            for oldpath, newpath, id, kind, text_modified, meta_modified \
361
249
                    in delta.renamed:
362
 
                self.model.append(titer, [ oldpath, newpath ])
 
250
                self.model.append(titer, [oldpath, newpath])
363
251
 
364
252
        if len(delta.modified):
365
 
            titer = self.model.append(None, [ "Modified", None ])
 
253
            titer = self.model.append(None, ["Modified", None])
366
254
            for path, id, kind, text_modified, meta_modified in delta.modified:
367
 
                self.model.append(titer, [ path, path ])
 
255
                self.model.append(titer, [path, path])
368
256
 
369
257
        self.treeview.expand_all()
 
258
        self.diff_view.show_diff(None)
370
259
 
371
260
    def set_file(self, file_path):
 
261
        """Select the current file to display"""
372
262
        tv_path = None
373
263
        for data in self.model:
374
264
            for child in data.iterchildren():
376
266
                    tv_path = child.path
377
267
                    break
378
268
        if tv_path is None:
379
 
            raise NoSuchFile(file_path)
380
 
        self.treeview.set_cursor(tv_path)
 
269
            raise errors.NoSuchFile(file_path)
 
270
        self.treeview.set_cursor(tv_path, None, False)
381
271
        self.treeview.scroll_to_cell(tv_path)
382
272
 
383
273
    def _treeview_cursor_cb(self, *args):
384
274
        """Callback for when the treeview cursor changes."""
385
275
        (path, col) = self.treeview.get_cursor()
386
 
        specific_files = [ self.model[path][1] ]
387
 
        if specific_files == [ None ]:
388
 
            return
389
 
        elif specific_files == [ "" ]:
 
276
        if path is None:
 
277
            return
 
278
        specific_files = [self.model[path][1]]
 
279
        if specific_files == [None]:
 
280
            return
 
281
        elif specific_files == [""]:
390
282
            specific_files = None
391
283
 
392
284
        self.diff_view.show_diff(specific_files)
393
285
 
 
286
    def _on_wraplines_toggled(self, widget=None, wrap=False):
 
287
        """Callback for when the wrap lines checkbutton is toggled"""
 
288
        if wrap or widget.get_active():
 
289
            self.diff_view.sourceview.set_wrap_mode(Gtk.WrapMode.WORD)
 
290
        else:
 
291
            self.diff_view.sourceview.set_wrap_mode(Gtk.WrapMode.NONE)
 
292
 
394
293
 
395
294
class DiffWindow(Window):
396
295
    """Diff window.
399
298
    differences between two revisions on a branch.
400
299
    """
401
300
 
402
 
    def __init__(self, parent=None):
403
 
        Window.__init__(self, parent)
 
301
    SHOW_WIDGETS = True
 
302
 
 
303
    def __init__(self, parent=None, operations=None):
 
304
        super(DiffWindow, self).__init__(parent=parent)
404
305
        self.set_border_width(0)
405
 
        self.set_title("bzrk diff")
 
306
        self.set_title("bzr diff")
406
307
 
407
308
        # Use two thirds of the screen by default
408
309
        screen = self.get_screen()
410
311
        width = int(monitor.width * 0.66)
411
312
        height = int(monitor.height * 0.66)
412
313
        self.set_default_size(width, height)
413
 
 
414
 
        self.construct()
415
 
 
416
 
    def construct(self):
 
314
        self.construct(operations)
 
315
 
 
316
    def construct(self, operations):
417
317
        """Construct the window contents."""
418
 
        self.vbox = gtk.VBox()
 
318
        self.vbox = Gtk.VBox()
419
319
        self.add(self.vbox)
420
 
        self.vbox.show()
421
 
        hbox = self._get_button_bar()
 
320
        if self.SHOW_WIDGETS:
 
321
            self.vbox.show()
 
322
        self.diff = DiffWidget()
 
323
        self.vbox.pack_end(self.diff, True, True, 0)
 
324
        if self.SHOW_WIDGETS:
 
325
            self.diff.show_all()
 
326
        # Build after DiffWidget to connect signals
 
327
        menubar = self._get_menu_bar()
 
328
        self.vbox.pack_start(menubar, False, False, 0)
 
329
        hbox = self._get_button_bar(operations)
422
330
        if hbox is not None:
423
 
            self.vbox.pack_start(hbox, expand=False, fill=True)
424
 
        self.diff = DiffWidget()
425
 
        self.vbox.add(self.diff)
426
 
        self.diff.show_all()
427
 
 
428
 
    def _get_button_bar(self):
429
 
        return None
430
 
 
431
 
    def set_diff_text(self, description, lines):
432
 
        self.diff.set_diff_text(lines)
433
 
        self.set_title(description + " - bzrk diff")
 
331
            self.vbox.pack_start(hbox, False, True, 0)
 
332
 
 
333
    def _get_menu_bar(self):
 
334
        menubar = Gtk.MenuBar()
 
335
        # View menu
 
336
        mb_view = Gtk.MenuItem.new_with_mnemonic(_i18n("_View"))
 
337
        mb_view_menu = Gtk.Menu()
 
338
        mb_view_wrapsource = Gtk.CheckMenuItem.new_with_mnemonic(
 
339
            _i18n("Wrap _Long Lines"))
 
340
        mb_view_wrapsource.connect('activate', self.diff._on_wraplines_toggled)
 
341
        mb_view_menu.append(mb_view_wrapsource)
 
342
        mb_view.set_submenu(mb_view_menu)
 
343
        menubar.append(mb_view)
 
344
        if self.SHOW_WIDGETS:
 
345
            menubar.show_all()
 
346
        return menubar
 
347
 
 
348
    def _get_button_bar(self, operations):
 
349
        """Return a button bar to use.
 
350
 
 
351
        :return: None, meaning that no button bar will be used.
 
352
        """
 
353
        if operations is None:
 
354
            return None
 
355
        hbox = Gtk.HButtonBox()
 
356
        hbox.set_layout(Gtk.ButtonBoxStyle.START)
 
357
        for title, method in operations:
 
358
            merge_button = Gtk.Button(title)
 
359
            if self.SHOW_WIDGETS:
 
360
                merge_button.show()
 
361
            merge_button.set_relief(Gtk.ReliefStyle.NONE)
 
362
            merge_button.connect("clicked", method)
 
363
            hbox.pack_start(merge_button, False, True, 0)
 
364
        if self.SHOW_WIDGETS:
 
365
            hbox.show()
 
366
        return hbox
 
367
 
 
368
    def _get_merge_target(self):
 
369
        d = Gtk.FileChooserDialog('Merge branch', self,
 
370
                                  Gtk.FileChooserAction.SELECT_FOLDER,
 
371
                                  buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK,
 
372
                                           Gtk.STOCK_CANCEL,
 
373
                                           Gtk.ResponseType.CANCEL,))
 
374
        try:
 
375
            result = d.run()
 
376
            if result != Gtk.ResponseType.OK:
 
377
                raise SelectCancelled()
 
378
            return d.get_current_folder_uri()
 
379
        finally:
 
380
            d.destroy()
 
381
 
 
382
    def _merge_successful(self):
 
383
        # No conflicts found.
 
384
        info_dialog(_i18n('Merge successful'),
 
385
                    _i18n('All changes applied successfully.'))
 
386
 
 
387
    def _conflicts(self):
 
388
        warning_dialog(_i18n('Conflicts encountered'),
 
389
                       _i18n('Please resolve the conflicts manually'
 
390
                             ' before committing.'))
 
391
 
 
392
    def _handle_error(self, e):
 
393
        error_dialog('Error', str(e))
 
394
 
 
395
    def _get_save_path(self, basename):
 
396
        d = Gtk.FileChooserDialog('Save As', self,
 
397
                                  Gtk.FileChooserAction.SAVE,
 
398
                                  buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK,
 
399
                                           Gtk.STOCK_CANCEL,
 
400
                                           Gtk.ResponseType.CANCEL,))
 
401
        d.set_current_name(basename)
 
402
        try:
 
403
            result = d.run()
 
404
            if result != Gtk.ResponseType.OK:
 
405
                raise SelectCancelled()
 
406
            return urlutils.local_path_from_url(d.get_uri())
 
407
        finally:
 
408
            d.destroy()
434
409
 
435
410
    def set_diff(self, description, rev_tree, parent_tree):
436
411
        """Set the differences showed by this window.
445
420
        self.diff.set_file(file_path)
446
421
 
447
422
 
448
 
class MergeDirectiveWindow(DiffWindow):
449
 
 
450
 
    def __init__(self, directive, parent=None):
451
 
        DiffWindow.__init__(self, parent)
452
 
        self._merge_target = None
 
423
class DiffController(object):
 
424
 
 
425
    def __init__(self, path, patch, window=None, allow_dirty=False):
 
426
        self.path = path
 
427
        self.patch = patch
 
428
        self.allow_dirty = allow_dirty
 
429
        if window is None:
 
430
            window = DiffWindow(operations=self._provide_operations())
 
431
            self.initialize_window(window)
 
432
        self.window = window
 
433
 
 
434
    def initialize_window(self, window):
 
435
        window.diff.set_diff_text_sections(self.get_diff_sections())
 
436
        window.set_title(self.path + " - diff")
 
437
 
 
438
    def get_diff_sections(self):
 
439
        yield "Complete Diff", None, ''.join(self.patch)
 
440
        # allow_dirty was added to parse_patches in bzrlib 2.2b1
 
441
        if 'allow_dirty' in inspect.getargspec(parse_patches).args:
 
442
            patches = parse_patches(self.patch, allow_dirty=self.allow_dirty)
 
443
        else:
 
444
            patches = parse_patches(self.patch)
 
445
        for patch in patches:
 
446
            oldname = patch.oldname.split('\t')[0]
 
447
            newname = patch.newname.split('\t')[0]
 
448
            yield oldname, newname, str(patch)
 
449
 
 
450
    def perform_save(self, window):
 
451
        try:
 
452
            save_path = self.window._get_save_path(osutils.basename(self.path))
 
453
        except SelectCancelled:
 
454
            return
 
455
        source = open(self.path, 'rb')
 
456
        try:
 
457
            target = open(save_path, 'wb')
 
458
            try:
 
459
                osutils.pumpfile(source, target)
 
460
            finally:
 
461
                target.close()
 
462
        finally:
 
463
            source.close()
 
464
 
 
465
    def _provide_operations(self):
 
466
        return [('Save', self.perform_save)]
 
467
 
 
468
 
 
469
class MergeDirectiveController(DiffController):
 
470
 
 
471
    def __init__(self, path, directive, window=None):
 
472
        super(MergeDirectiveController, self).__init__(
 
473
            path, directive.patch.splitlines(True), window)
453
474
        self.directive = directive
454
 
 
455
 
    def _get_button_bar(self):
456
 
        merge_button = gtk.Button('Merge')
457
 
        merge_button.show()
458
 
        merge_button.set_relief(gtk.RELIEF_NONE)
459
 
        merge_button.connect("clicked", self.perform_merge)
460
 
 
461
 
        hbox = gtk.HButtonBox()
462
 
        hbox.set_layout(gtk.BUTTONBOX_START)
463
 
        hbox.pack_start(merge_button, expand=False, fill=True)
464
 
        hbox.show()
465
 
        return hbox
 
475
        self.merge_target = None
 
476
 
 
477
    def _provide_operations(self):
 
478
        return [('Merge', self.perform_merge), ('Save', self.perform_save)]
466
479
 
467
480
    def perform_merge(self, window):
468
 
        try:
469
 
            tree = self._get_merge_target()
470
 
        except SelectCancelled:
471
 
            return
 
481
        if self.merge_target is None:
 
482
            try:
 
483
                self.merge_target = self.window._get_merge_target()
 
484
            except SelectCancelled:
 
485
                return
 
486
        tree = workingtree.WorkingTree.open(self.merge_target)
472
487
        tree.lock_write()
473
488
        try:
474
489
            try:
475
 
                merger, verified = _mod_merge.Merger.from_mergeable(tree,
476
 
                    self.directive, progress.DummyProgress())
477
 
                merger.check_basis(True)
 
490
                if tree.has_changes():
 
491
                    raise errors.UncommittedChanges(tree)
 
492
                merger, verified = _mod_merge.Merger.from_mergeable(
 
493
                    tree, self.directive, pb=None)
478
494
                merger.merge_type = _mod_merge.Merge3Merger
479
495
                conflict_count = merger.do_merge()
480
496
                merger.set_pending()
481
497
                if conflict_count == 0:
482
 
                    # No conflicts found.
483
 
                    info_dialog(_('Merge successful'),
484
 
                                _('All changes applied successfully.'))
 
498
                    self.window._merge_successful()
485
499
                else:
 
500
                    self.window._conflicts()
486
501
                    # There are conflicts to be resolved.
487
 
                    warning_dialog(_('Conflicts encountered'),
488
 
                                   _('Please resolve the conflicts manually'
489
 
                                     ' before committing.'))
490
 
                self.destroy()
 
502
                self.window.destroy()
491
503
            except Exception, e:
492
 
                error_dialog('Error', str(e))
 
504
                self.window._handle_error(e)
493
505
        finally:
494
506
            tree.unlock()
495
507
 
496
 
    def _get_merge_target(self):
497
 
        if self._merge_target is not None:
498
 
            return self._merge_target
499
 
        d = gtk.FileChooserDialog('Merge branch', self,
500
 
                                  gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
501
 
                                  buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
502
 
                                           gtk.STOCK_CANCEL,
503
 
                                           gtk.RESPONSE_CANCEL,))
504
 
        try:
505
 
            result = d.run()
506
 
            if result == gtk.RESPONSE_OK:
507
 
                uri = d.get_current_folder_uri()
508
 
                return workingtree.WorkingTree.open(uri)
509
 
            else:
510
 
                raise SelectCancelled()
511
 
        finally:
512
 
            d.destroy()
513
 
 
514
 
 
515
 
def _iter_changes_to_status(source, target):
 
508
 
 
509
def iter_changes_to_status(source, target):
516
510
    """Determine the differences between trees.
517
511
 
518
 
    This is a wrapper around _iter_changes which just yields more
 
512
    This is a wrapper around iter_changes which just yields more
519
513
    understandable results.
520
514
 
521
515
    :param source: The source tree (basis tree)
528
522
    renamed_and_modified = 'renamed and modified'
529
523
    modified = 'modified'
530
524
    kind_changed = 'kind changed'
 
525
    missing = 'missing'
531
526
 
532
527
    # TODO: Handle metadata changes
533
528
 
537
532
        source.lock_read()
538
533
        try:
539
534
            for (file_id, paths, changed_content, versioned, parent_ids, names,
540
 
                 kinds, executables) in target._iter_changes(source):
 
535
                 kinds, executables) in target.iter_changes(source):
541
536
 
542
537
                # Skip the root entry if it isn't very interesting
543
538
                if parent_ids == (None, None):
548
543
                    source_marker = ''
549
544
                else:
550
545
                    source_marker = osutils.kind_marker(kinds[0])
 
546
 
551
547
                if kinds[1] is None:
552
 
                    assert kinds[0] is not None
553
 
                    marker = osutils.kind_marker(kinds[0])
 
548
                    if kinds[0] is None:
 
549
                        # We assume bzr will flag only files in that case,
 
550
                        # there may be a bzr bug there as only files seems to
 
551
                        # not receive any kind.
 
552
                        marker = osutils.kind_marker('file')
 
553
                    else:
 
554
                        marker = osutils.kind_marker(kinds[0])
554
555
                else:
555
556
                    marker = osutils.kind_marker(kinds[1])
556
557
 
558
559
                if real_path is None:
559
560
                    real_path = paths[0]
560
561
                assert real_path is not None
561
 
                display_path = real_path + marker
562
562
 
563
563
                present_source = versioned[0] and kinds[0] is not None
564
564
                present_target = versioned[1] and kinds[1] is not None
565
565
 
566
 
                if present_source != present_target:
 
566
                if kinds[0] is None and kinds[1] is None:
 
567
                    change_type = missing
 
568
                    display_path = real_path + marker
 
569
                elif present_source != present_target:
567
570
                    if present_target:
568
571
                        change_type = added
569
572
                    else:
570
573
                        assert present_source
571
574
                        change_type = removed
 
575
                    display_path = real_path + marker
572
576
                elif names[0] != names[1] or parent_ids[0] != parent_ids[1]:
573
577
                    # Renamed
574
578
                    if changed_content or executables[0] != executables[1]:
582
586
                    change_type = kind_changed
583
587
                    display_path = (paths[0] + source_marker
584
588
                                    + ' => ' + paths[1] + marker)
585
 
                elif changed_content is True or executables[0] != executables[1]:
 
589
                elif changed_content or executables[0] != executables[1]:
586
590
                    change_type = modified
 
591
                    display_path = real_path + marker
587
592
                else:
588
593
                    assert False, "How did we get here?"
589
594