/b-gtk/fix-viz

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

« back to all changes in this revision

Viewing changes to diff.py

  • Committer: Curtis Hovey
  • Date: 2011-09-05 03:44:26 UTC
  • mto: This revision was merged to the branch mainline in revision 741.
  • Revision ID: sinzui.is@verizon.net-20110905034426-p98pxnay9rmzkr99
Fix the initializer for many classes.
Replace Gtk.Dialog.vbox with .get_content_area().

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