/b-gtk/fix-viz

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/b-gtk/fix-viz

« back to all changes in this revision

Viewing changes to diff.py

  • Committer: Andrew Bennetts
  • Date: 2008-04-29 08:17:01 UTC
  • mto: This revision was merged to the branch mainline in revision 476.
  • Revision ID: andrew@puzzling.org-20080429081701-2pu9uteic8o88nre
Simple hack to fix gannotate.

Show diffs side-by-side

added added

removed removed

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