/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: Daniel Schierbeck
  • Date: 2008-01-13 14:12:49 UTC
  • mto: (423.1.2 trunk)
  • mto: This revision was merged to the branch mainline in revision 429.
  • Revision ID: daniel.schierbeck@gmail.com-20080113141249-gd0i2lknr3yik55r
Moved branch view to its own package.

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