/b-gtk/fix-viz

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/b-gtk/fix-viz
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
1
# -*- coding: UTF-8 -*-
2
"""Difference window.
3
4
This module contains the code to manage the diff window which shows
5
the changes made between two revisions on a branch.
6
"""
7
8
__copyright__ = "Copyright © 2005 Canonical Ltd."
9
__author__    = "Scott James Remnant <scott@ubuntu.com>"
10
11
12
from cStringIO import StringIO
13
252 by Aaron Bentley
Fix test suite
14
import pygtk
15
pygtk.require("2.0")
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
16
import gtk
17
import pango
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
18
import os
19
import re
76 by Jelmer Vernooij
Replace non-UTF8 characters rather than generating an exception (fixes #44677).
20
import sys
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
21
22
try:
23
    import gtksourceview
24
    have_gtksourceview = True
25
except ImportError:
26
    have_gtksourceview = False
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
27
try:
28
    import gconf
29
    have_gconf = True
30
except ImportError:
31
    have_gconf = False
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
32
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
33
from bzrlib import osutils
34
from bzrlib.diff import show_diff_trees, internal_diff
59.2.4 by Aaron Bentley
Teach gdiff to accept a single file argument
35
from bzrlib.errors import NoSuchFile
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
36
from bzrlib.trace import warning
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
37
38
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
39
class DiffView(gtk.ScrolledWindow):
278.1.4 by John Arbash Meinel
Just playing around.
40
    """This is the soft and chewy filling for a DiffWindow."""
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
41
51 by Jelmer Vernooij
Rework some of the parameters to DiffWindow.set_diff() to be
42
    def __init__(self):
278.1.4 by John Arbash Meinel
Just playing around.
43
        gtk.ScrolledWindow.__init__(self)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
44
45
        self.construct()
278.1.4 by John Arbash Meinel
Just playing around.
46
        self.rev_tree = None
47
        self.parent_tree = None
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
48
49
    def construct(self):
278.1.4 by John Arbash Meinel
Just playing around.
50
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
51
        self.set_shadow_type(gtk.SHADOW_IN)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
52
53
        if have_gtksourceview:
54
            self.buffer = gtksourceview.SourceBuffer()
55
            slm = gtksourceview.SourceLanguagesManager()
56
            gsl = slm.get_language_from_mime_type("text/x-patch")
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
57
            if have_gconf:
58
                self.apply_gedit_colors(gsl)
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
59
            self.apply_colordiff_colors(gsl)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
60
            self.buffer.set_language(gsl)
61
            self.buffer.set_highlight(True)
62
63
            sourceview = gtksourceview.SourceView(self.buffer)
64
        else:
65
            self.buffer = gtk.TextBuffer()
66
            sourceview = gtk.TextView(self.buffer)
67
68
        sourceview.set_editable(False)
69
        sourceview.modify_font(pango.FontDescription("Monospace"))
278.1.4 by John Arbash Meinel
Just playing around.
70
        self.add(sourceview)
10 by Scott James Remnant
Add an extra window type, clicking the little icons next to a parent
71
        sourceview.show()
72
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
73
    @staticmethod
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
74
    def apply_gedit_colors(lang):
75
        """Set style for lang to that specified in gedit configuration.
76
77
        This method needs the gconf module.
278.1.4 by John Arbash Meinel
Just playing around.
78
232.1.3 by Adeodato Simó
Support setting diff colors from gedit's syntax highlighting config too.
79
        :param lang: a gtksourceview.SourceLanguage object.
80
        """
81
        GEDIT_SYNTAX_PATH = '/apps/gedit-2/preferences/syntax_highlighting'
82
        GEDIT_LANG_PATH = GEDIT_SYNTAX_PATH + '/' + lang.get_id()
83
84
        client = gconf.client_get_default()
85
        client.add_dir(GEDIT_LANG_PATH, gconf.CLIENT_PRELOAD_NONE)
86
87
        for tag in lang.get_tags():
88
            tag_id = tag.get_id()
89
            gconf_key = GEDIT_LANG_PATH + '/' + tag_id
90
            style_string = client.get_string(gconf_key)
91
92
            if style_string is None:
93
                continue
94
95
            # function to get a bool from a string that's either '0' or '1'
96
            string_bool = lambda x: bool(int(x))
97
98
            # style_string is a string like "2/#FFCCAA/#000000/0/1/0/0"
99
            # values are: mask, fg, bg, italic, bold, underline, strike
100
            # this packs them into (str_value, attr_name, conv_func) tuples
101
            items = zip(style_string.split('/'), ['mask', 'foreground',
102
                'background', 'italic', 'bold', 'underline', 'strikethrough' ],
103
                [ int, gtk.gdk.color_parse, gtk.gdk.color_parse, string_bool,
104
                    string_bool, string_bool, string_bool ]
105
            )
106
107
            style = gtksourceview.SourceTagStyle()
108
109
            # XXX The mask attribute controls whether the present values of
110
            # foreground and background color should in fact be used. Ideally
111
            # (and that's what gedit does), one could set all three attributes,
112
            # and let the TagStyle object figure out which colors to use.
113
            # However, in the GtkSourceview python bindings, the mask attribute
114
            # is read-only, and it's derived instead from the colors being
115
            # set or not. This means that we have to sometimes refrain from
116
            # setting fg or bg colors, depending on the value of the mask.
117
            # This code could go away if mask were writable.
118
            mask = int(items[0][0])
119
            if not (mask & 1): # GTK_SOURCE_TAG_STYLE_USE_BACKGROUND
120
                items[2:3] = []
121
            if not (mask & 2): # GTK_SOURCE_TAG_STYLE_USE_FOREGROUND
122
                items[1:2] = []
123
            items[0:1] = [] # skip the mask unconditionally
124
125
            for value, attr, func in items:
126
                try:
127
                    value = func(value)
128
                except ValueError:
129
                    warning('gconf key %s contains an invalid value: %s'
130
                            % gconf_key, value)
131
                else:
132
                    setattr(style, attr, value)
133
134
            lang.set_tag_style(tag_id, style)
135
136
    @staticmethod
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
137
    def apply_colordiff_colors(lang):
138
        """Set style colors for lang using the colordiff configuration file.
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
139
140
        Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
141
232.1.2 by Adeodato Simó
Rename apply_colordiffrc to apply_colordiff_colors, improve docstring.
142
        :param lang: a "Diff" gtksourceview.SourceLanguage object.
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
143
        """
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:
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
154
                    colors.update(DiffView.parse_colordiffrc(f))
232.1.1 by Adeodato Simó
Read ~/.colordiffrc to set colors in the diff window.
155
                    f.close()
156
157
        if not colors:
158
            # ~/.colordiffrc does not exist
159
            return
160
161
        mapping = {
162
                # map GtkSourceView tags to colordiff names
163
                # since GSV is richer, accept new names for extra bits,
164
                # defaulting to old names if they're not present
165
                'Added@32@line': ['newtext'],
166
                'Removed@32@line': ['oldtext'],
167
                'Location': ['location', 'diffstuff'],
168
                'Diff@32@file': ['file', 'diffstuff'],
169
                'Special@32@case': ['specialcase', 'diffstuff'],
170
        }
171
172
        for tag in lang.get_tags():
173
            tag_id = tag.get_id()
174
            keys = mapping.get(tag_id, [])
175
            color = None
176
177
            for key in keys:
178
                color = colors.get(key, None)
179
                if color is not None:
180
                    break
181
182
            if color is None:
183
                continue
184
185
            style = gtksourceview.SourceTagStyle()
186
            try:
187
                style.foreground = gtk.gdk.color_parse(color)
188
            except ValueError:
189
                warning('not a valid color: %s' % color)
190
            else:
191
                lang.set_tag_style(tag_id, style)
232.1.4 by Adeodato Simó
Add a test for parse_colordiffrc, and fix the function to handle empty lines.
192
193
    @staticmethod
194
    def parse_colordiffrc(fileobj):
195
        """Parse fileobj as a colordiff configuration file.
278.1.4 by John Arbash Meinel
Just playing around.
196
232.1.4 by Adeodato Simó
Add a test for parse_colordiffrc, and fix the function to handle empty lines.
197
        :return: A dict with the key -> value pairs.
198
        """
199
        colors = {}
200
        for line in fileobj:
201
            if re.match(r'^\s*#', line):
202
                continue
203
            if '=' not in line:
204
                continue
205
            key, val = line.split('=', 1)
206
            colors[key.strip()] = val.strip()
207
        return colors
208
278.1.4 by John Arbash Meinel
Just playing around.
209
    def set_trees(self, rev_tree, parent_tree):
210
        self.rev_tree = rev_tree
211
        self.parent_tree = parent_tree
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
212
#        self._build_delta()
213
214
#    def _build_delta(self):
215
#        self.parent_tree.lock_read()
216
#        self.rev_tree.lock_read()
217
#        try:
218
#            self.delta = _iter_changes_to_status(self.parent_tree, self.rev_tree)
219
#            self.path_to_status = {}
220
#            self.path_to_diff = {}
221
#            source_inv = self.parent_tree.inventory
222
#            target_inv = self.rev_tree.inventory
223
#            for (file_id, real_path, change_type, display_path) in self.delta:
224
#                self.path_to_status[real_path] = u'=== %s %s' % (change_type, display_path)
225
#                if change_type in ('modified', 'renamed and modified'):
226
#                    source_ie = source_inv[file_id]
227
#                    target_ie = target_inv[file_id]
228
#                    sio = StringIO()
229
#                    source_ie.diff(internal_diff, *old path, *old_tree,
230
#                                   *new_path, target_ie, self.rev_tree,
231
#                                   sio)
232
#                    self.path_to_diff[real_path] = 
233
#
234
#        finally:
235
#            self.rev_tree.unlock()
236
#            self.parent_tree.unlock()
278.1.4 by John Arbash Meinel
Just playing around.
237
238
    def show_diff(self, specific_files):
239
        s = StringIO()
278.1.18 by John Arbash Meinel
Start checking the diff view is correct.
240
        show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files,
241
                        old_label='', new_label='',
242
                        # path_encoding=sys.getdefaultencoding()
243
                        # The default is utf-8, but we interpret the file
244
                        # contents as getdefaultencoding(), so we should
245
                        # probably try to make the paths in the same encoding.
246
                        )
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
247
        # str.decode(encoding, 'replace') doesn't do anything. Because if a
248
        # character is not valid in 'encoding' there is nothing to replace, the
249
        # 'replace' is for 'str.encode()'
250
        try:
251
            decoded = s.getvalue().decode(sys.getdefaultencoding())
252
        except UnicodeDecodeError:
253
            try:
254
                decoded = s.getvalue().decode('UTF-8')
255
            except UnicodeDecodeError:
256
                decoded = s.getvalue().decode('iso-8859-1')
257
                # This always works, because every byte has a valid
258
                # mapping from iso-8859-1 to Unicode
259
        # TextBuffer must contain pure UTF-8 data
260
        self.buffer.set_text(decoded.encode('UTF-8'))
278.1.4 by John Arbash Meinel
Just playing around.
261
262
263
class DiffWindow(gtk.Window):
264
    """Diff window.
265
266
    This object represents and manages a single window containing the
267
    differences between two revisions on a branch.
268
    """
269
270
    def __init__(self):
271
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
272
        self.set_border_width(0)
273
        self.set_title("bzrk diff")
274
275
        # Use two thirds of the screen by default
276
        screen = self.get_screen()
277
        monitor = screen.get_monitor_geometry(0)
278
        width = int(monitor.width * 0.66)
279
        height = int(monitor.height * 0.66)
280
        self.set_default_size(width, height)
281
282
        self.construct()
283
284
    def construct(self):
285
        """Construct the window contents."""
286
        # The   window  consists  of   a  pane   containing:  the
287
        # hierarchical list  of files on  the left, and  the diff
288
        # for the currently selected file on the right.
289
        pane = gtk.HPaned()
290
        self.add(pane)
291
        pane.show()
292
293
        # 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)
297
        pane.pack1(scrollwin)
298
        scrollwin.show()
299
300
        self.model = gtk.TreeStore(str, str)
301
        self.treeview = gtk.TreeView(self.model)
302
        self.treeview.set_headers_visible(False)
303
        self.treeview.set_search_column(1)
304
        self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
305
        scrollwin.add(self.treeview)
306
        self.treeview.show()
307
308
        cell = gtk.CellRendererText()
309
        cell.set_property("width-chars", 20)
310
        column = gtk.TreeViewColumn()
311
        column.pack_start(cell, expand=True)
312
        column.add_attribute(cell, "text", 0)
313
        self.treeview.append_column(column)
314
315
        # The diffs of the  selected file: a scrollable source or
316
        # text view
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
317
        self.diff_view = DiffView()
318
        pane.pack2(self.diff_view)
319
        self.diff_view.show()
278.1.4 by John Arbash Meinel
Just playing around.
320
321
    def set_diff(self, description, rev_tree, parent_tree):
322
        """Set the differences showed by this window.
323
324
        Compares the two trees and populates the window with the
325
        differences.
326
        """
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
327
        self.diff_view.set_trees(rev_tree, parent_tree)
278.1.4 by John Arbash Meinel
Just playing around.
328
        self.rev_tree = rev_tree
329
        self.parent_tree = parent_tree
330
331
        self.model.clear()
332
        delta = self.rev_tree.changes_from(self.parent_tree)
333
334
        self.model.append(None, [ "Complete Diff", "" ])
335
336
        if len(delta.added):
337
            titer = self.model.append(None, [ "Added", None ])
338
            for path, id, kind in delta.added:
339
                self.model.append(titer, [ path, path ])
340
341
        if len(delta.removed):
342
            titer = self.model.append(None, [ "Removed", None ])
343
            for path, id, kind in delta.removed:
344
                self.model.append(titer, [ path, path ])
345
346
        if len(delta.renamed):
347
            titer = self.model.append(None, [ "Renamed", None ])
348
            for oldpath, newpath, id, kind, text_modified, meta_modified \
349
                    in delta.renamed:
350
                self.model.append(titer, [ oldpath, newpath ])
351
352
        if len(delta.modified):
353
            titer = self.model.append(None, [ "Modified", None ])
354
            for path, id, kind, text_modified, meta_modified in delta.modified:
355
                self.model.append(titer, [ path, path ])
356
357
        self.treeview.expand_all()
358
        self.set_title(description + " - bzrk diff")
359
360
    def set_file(self, file_path):
361
        tv_path = None
362
        for data in self.model:
363
            for child in data.iterchildren():
364
                if child[0] == file_path or child[1] == file_path:
365
                    tv_path = child.path
366
                    break
367
        if tv_path is None:
368
            raise NoSuchFile(file_path)
369
        self.treeview.set_cursor(tv_path)
370
        self.treeview.scroll_to_cell(tv_path)
371
372
    def _treeview_cursor_cb(self, *args):
373
        """Callback for when the treeview cursor changes."""
374
        (path, col) = self.treeview.get_cursor()
375
        specific_files = [ self.model[path][1] ]
376
        if specific_files == [ None ]:
377
            return
378
        elif specific_files == [ "" ]:
379
            specific_files = None
380
278.1.12 by John Arbash Meinel
Delay computing the delta, and clean up some of the diff view names.
381
        self.diff_view.show_diff(specific_files)
278.1.29 by John Arbash Meinel
Start testing with Unicode data.
382
383
384
def _iter_changes_to_status(source, target):
385
    """Determine the differences between trees.
386
387
    This is a wrapper around _iter_changes which just yields more
388
    understandable results.
389
390
    :param source: The source tree (basis tree)
391
    :param target: The target tree
392
    :return: A list of (file_id, real_path, change_type, display_path)
393
    """
394
    added = 'added'
395
    removed = 'removed'
396
    renamed = 'renamed'
397
    renamed_and_modified = 'renamed and modified'
398
    modified = 'modified'
399
    kind_changed = 'kind changed'
400
401
    # TODO: Handle metadata changes
402
403
    status = []
404
    target.lock_read()
405
    try:
406
        source.lock_read()
407
        try:
408
            for (file_id, paths, changed_content, versioned, parent_ids, names,
409
                 kinds, executables) in target._iter_changes(source):
410
411
                # Skip the root entry if it isn't very interesting
412
                if parent_ids == (None, None):
413
                    continue
414
415
                change_type = None
416
                if kinds[0] is None:
417
                    source_marker = ''
418
                else:
419
                    source_marker = osutils.kind_marker(kinds[0])
420
                if kinds[1] is None:
421
                    assert kinds[0] is not None
422
                    marker = osutils.kind_marker(kinds[0])
423
                else:
424
                    marker = osutils.kind_marker(kinds[1])
425
426
                real_path = paths[1]
427
                if real_path is None:
428
                    real_path = paths[0]
429
                assert real_path is not None
430
                display_path = real_path + marker
431
432
                present_source = versioned[0] and kinds[0] is not None
433
                present_target = versioned[1] and kinds[1] is not None
434
435
                if present_source != present_target:
436
                    if present_target:
437
                        change_type = added
438
                    else:
439
                        assert present_source
440
                        change_type = removed
441
                elif names[0] != names[1] or parent_ids[0] != parent_ids[1]:
442
                    # Renamed
443
                    if changed_content or executables[0] != executables[1]:
444
                        # and modified
445
                        change_type = renamed_and_modified
446
                    else:
447
                        change_type = renamed
448
                    display_path = (paths[0] + source_marker
449
                                    + ' => ' + paths[1] + marker)
450
                elif kinds[0] != kinds[1]:
451
                    change_type = kind_changed
452
                    display_path = (paths[0] + source_marker
453
                                    + ' => ' + paths[1] + marker)
454
                elif changed_content is True or executables[0] != executables[1]:
455
                    change_type = modified
456
                else:
457
                    assert False, "How did we get here?"
458
459
                status.append((file_id, real_path, change_type, display_path))
460
        finally:
461
            source.unlock()
462
    finally:
463
        target.unlock()
464
465
    return status