/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: Martin Pool
  • Date: 2010-05-27 03:07:30 UTC
  • mfrom: (688.1.5 201956-help)
  • Revision ID: mbp@canonical.com-20100527030730-os0opv1xroetccm9
Make find/goto more discoverable

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
 
18
17
import os
19
18
import re
20
19
import sys
 
20
try:
 
21
    from xml.etree.ElementTree import Element, SubElement, tostring
 
22
except ImportError:
 
23
    from elementtree.ElementTree import Element, SubElement, tostring
21
24
 
22
25
try:
23
 
    import gtksourceview
 
26
    import gtksourceview2
24
27
    have_gtksourceview = True
25
28
except ImportError:
26
29
    have_gtksourceview = False
30
33
except ImportError:
31
34
    have_gconf = False
32
35
 
33
 
from bzrlib import osutils
 
36
from bzrlib import (
 
37
    errors,
 
38
    merge as _mod_merge,
 
39
    osutils,
 
40
    urlutils,
 
41
    workingtree,
 
42
)
34
43
from bzrlib.diff import show_diff_trees, internal_diff
35
 
from bzrlib.errors import NoSuchFile
 
44
from bzrlib.patches import parse_patches
36
45
from bzrlib.trace import warning
 
46
from bzrlib.plugins.gtk import _i18n
37
47
from bzrlib.plugins.gtk.window import Window
38
 
 
39
 
 
40
 
class DiffView(gtk.ScrolledWindow):
41
 
    """This is the soft and chewy filling for a DiffWindow."""
 
48
from dialog import error_dialog, info_dialog, warning_dialog
 
49
 
 
50
 
 
51
def fallback_guess_language(slm, content_type):
 
52
    for lang_id in slm.get_language_ids():
 
53
        lang = slm.get_language(lang_id)
 
54
        if "text/x-patch" in lang.get_mime_types():
 
55
            return lang
 
56
    return None
 
57
 
 
58
 
 
59
class SelectCancelled(Exception):
 
60
 
 
61
    pass
 
62
 
 
63
 
 
64
class DiffFileView(gtk.ScrolledWindow):
 
65
    """Window for displaying diffs from a diff file"""
42
66
 
43
67
    def __init__(self):
44
68
        gtk.ScrolledWindow.__init__(self)
45
 
 
46
69
        self.construct()
47
 
        self.rev_tree = None
48
 
        self.parent_tree = None
 
70
        self._diffs = {}
49
71
 
50
72
    def construct(self):
51
73
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
52
74
        self.set_shadow_type(gtk.SHADOW_IN)
53
75
 
54
76
        if have_gtksourceview:
55
 
            self.buffer = gtksourceview.SourceBuffer()
56
 
            slm = gtksourceview.SourceLanguagesManager()
57
 
            gsl = slm.get_language_from_mime_type("text/x-patch")
 
77
            self.buffer = gtksourceview2.Buffer()
 
78
            slm = gtksourceview2.LanguageManager()
 
79
            guess_language = getattr(gtksourceview2.LanguageManager, 
 
80
                "guess_language", fallback_guess_language)
 
81
            gsl = guess_language(slm, content_type="text/x-patch")
58
82
            if have_gconf:
59
 
                self.apply_gedit_colors(gsl)
60
 
            self.apply_colordiff_colors(gsl)
 
83
                self.apply_gedit_colors(self.buffer)
 
84
            self.apply_colordiff_colors(self.buffer)
61
85
            self.buffer.set_language(gsl)
62
 
            self.buffer.set_highlight(True)
 
86
            self.buffer.set_highlight_syntax(True)
63
87
 
64
 
            sourceview = gtksourceview.SourceView(self.buffer)
 
88
            self.sourceview = gtksourceview2.View(self.buffer)
65
89
        else:
66
90
            self.buffer = gtk.TextBuffer()
67
 
            sourceview = gtk.TextView(self.buffer)
 
91
            self.sourceview = gtk.TextView(self.buffer)
68
92
 
69
 
        sourceview.set_editable(False)
70
 
        sourceview.modify_font(pango.FontDescription("Monospace"))
71
 
        self.add(sourceview)
72
 
        sourceview.show()
 
93
        self.sourceview.set_editable(False)
 
94
        self.sourceview.modify_font(pango.FontDescription("Monospace"))
 
95
        self.add(self.sourceview)
 
96
        self.sourceview.show()
73
97
 
74
98
    @staticmethod
75
 
    def apply_gedit_colors(lang):
76
 
        """Set style for lang to that specified in gedit configuration.
 
99
    def apply_gedit_colors(buf):
 
100
        """Set style to that specified in gedit configuration.
77
101
 
78
102
        This method needs the gconf module.
79
103
 
80
 
        :param lang: a gtksourceview.SourceLanguage object.
 
104
        :param buf: a gtksourceview2.Buffer object.
81
105
        """
82
 
        GEDIT_SYNTAX_PATH = '/apps/gedit-2/preferences/syntax_highlighting'
83
 
        GEDIT_LANG_PATH = GEDIT_SYNTAX_PATH + '/' + lang.get_id()
 
106
        GEDIT_SCHEME_PATH = '/apps/gedit-2/preferences/editor/colors/scheme'
84
107
 
85
108
        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):
 
109
        style_scheme_name = client.get_string(GEDIT_SCHEME_PATH)
 
110
        if style_scheme_name is not None:
 
111
            style_scheme = gtksourceview2.StyleSchemeManager().get_scheme(style_scheme_name)
 
112
            
 
113
            buf.set_style_scheme(style_scheme)
 
114
 
 
115
    @classmethod
 
116
    def apply_colordiff_colors(klass, buf):
139
117
        """Set style colors for lang using the colordiff configuration file.
140
118
 
141
119
        Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
142
120
 
143
 
        :param lang: a "Diff" gtksourceview.SourceLanguage object.
 
121
        :param buf: a "Diff" gtksourceview2.Buffer object.
144
122
        """
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
 
123
        scheme_manager = gtksourceview2.StyleSchemeManager()
 
124
        style_scheme = scheme_manager.get_scheme('colordiff')
 
125
        
 
126
        # if style scheme not found, we'll generate it from colordiffrc
 
127
        # TODO: reload if colordiffrc has changed.
 
128
        if style_scheme is None:
 
129
            colors = {}
 
130
 
 
131
            for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
 
132
                f = os.path.expanduser(f)
 
133
                if os.path.exists(f):
 
134
                    try:
 
135
                        f = file(f)
 
136
                    except IOError, e:
 
137
                        warning('could not open file %s: %s' % (f, str(e)))
 
138
                    else:
 
139
                        colors.update(klass.parse_colordiffrc(f))
 
140
                        f.close()
 
141
 
 
142
            if not colors:
 
143
                # ~/.colordiffrc does not exist
 
144
                return
 
145
            
 
146
            mapping = {
 
147
                # map GtkSourceView2 scheme styles to colordiff names
164
148
                # since GSV is richer, accept new names for extra bits,
165
149
                # defaulting to old names if they're not present
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)
 
150
                'diff:added-line': ['newtext'],
 
151
                'diff:removed-line': ['oldtext'],
 
152
                'diff:location': ['location', 'diffstuff'],
 
153
                'diff:file': ['file', 'diffstuff'],
 
154
                'diff:special-case': ['specialcase', 'diffstuff'],
 
155
            }
 
156
            
 
157
            converted_colors = {}
 
158
            for name, values in mapping.items():
 
159
                color = None
 
160
                for value in values:
 
161
                    color = colors.get(value, None)
 
162
                    if color is not None:
 
163
                        break
 
164
                if color is None:
 
165
                    continue
 
166
                converted_colors[name] = color
 
167
            
 
168
            # some xml magic to produce needed style scheme description
 
169
            e_style_scheme = Element('style-scheme')
 
170
            e_style_scheme.set('id', 'colordiff')
 
171
            e_style_scheme.set('_name', 'ColorDiff')
 
172
            e_style_scheme.set('version', '1.0')
 
173
            for name, color in converted_colors.items():
 
174
                style = SubElement(e_style_scheme, 'style')
 
175
                style.set('name', name)
 
176
                style.set('foreground', '#%s' % color)
 
177
            
 
178
            scheme_xml = tostring(e_style_scheme, 'UTF-8')
 
179
            if not os.path.exists(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles')):
 
180
                os.makedirs(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles'))
 
181
            file(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles/colordiff.xml'), 'w').write(scheme_xml)
 
182
            
 
183
            scheme_manager.force_rescan()
 
184
            style_scheme = scheme_manager.get_scheme('colordiff')
 
185
        
 
186
        buf.set_style_scheme(style_scheme)
193
187
 
194
188
    @staticmethod
195
189
    def parse_colordiffrc(fileobj):
216
210
#        self.parent_tree.lock_read()
217
211
#        self.rev_tree.lock_read()
218
212
#        try:
219
 
#            self.delta = _iter_changes_to_status(self.parent_tree, self.rev_tree)
 
213
#            self.delta = iter_changes_to_status(self.parent_tree, self.rev_tree)
220
214
#            self.path_to_status = {}
221
215
#            self.path_to_diff = {}
222
216
#            source_inv = self.parent_tree.inventory
237
231
#            self.parent_tree.unlock()
238
232
 
239
233
    def show_diff(self, specific_files):
 
234
        sections = []
 
235
        if specific_files is None:
 
236
            self.buffer.set_text(self._diffs[None])
 
237
        else:
 
238
            for specific_file in specific_files:
 
239
                sections.append(self._diffs[specific_file])
 
240
            self.buffer.set_text(''.join(sections))
 
241
 
 
242
 
 
243
class DiffView(DiffFileView):
 
244
    """This is the soft and chewy filling for a DiffWindow."""
 
245
 
 
246
    def __init__(self):
 
247
        DiffFileView.__init__(self)
 
248
        self.rev_tree = None
 
249
        self.parent_tree = None
 
250
 
 
251
    def show_diff(self, specific_files):
 
252
        """Show the diff for the specified files"""
240
253
        s = StringIO()
241
254
        show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files,
242
255
                        old_label='', new_label='',
261
274
        self.buffer.set_text(decoded.encode('UTF-8'))
262
275
 
263
276
 
264
 
class DiffWindow(Window):
265
 
    """Diff window.
 
277
class DiffWidget(gtk.HPaned):
 
278
    """Diff widget
266
279
 
267
 
    This object represents and manages a single window containing the
268
 
    differences between two revisions on a branch.
269
280
    """
270
 
 
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()
 
281
    def __init__(self):
 
282
        super(DiffWidget, self).__init__()
293
283
 
294
284
        # The file hierarchy: a scrollable treeview
295
285
        scrollwin = gtk.ScrolledWindow()
296
286
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
297
287
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
298
 
        pane.pack1(scrollwin)
 
288
        self.pack1(scrollwin)
299
289
        scrollwin.show()
300
 
 
 
290
        
301
291
        self.model = gtk.TreeStore(str, str)
302
292
        self.treeview = gtk.TreeView(self.model)
303
293
        self.treeview.set_headers_visible(False)
313
303
        column.add_attribute(cell, "text", 0)
314
304
        self.treeview.append_column(column)
315
305
 
 
306
    def set_diff_text(self, lines):
 
307
        """Set the current diff from a list of lines
 
308
 
 
309
        :param lines: The diff to show, in unified diff format
 
310
        """
316
311
        # The diffs of the  selected file: a scrollable source or
317
312
        # text view
318
 
        self.diff_view = DiffView()
319
 
        pane.pack2(self.diff_view)
 
313
 
 
314
    def set_diff_text_sections(self, sections):
 
315
        if getattr(self, 'diff_view', None) is None:
 
316
            self.diff_view = DiffFileView()
 
317
            self.pack2(self.diff_view)
320
318
        self.diff_view.show()
 
319
        for oldname, newname, patch in sections:
 
320
            self.diff_view._diffs[newname] = str(patch)
 
321
            if newname is None:
 
322
                newname = ''
 
323
            self.model.append(None, [oldname, newname])
 
324
        self.diff_view.show_diff(None)
321
325
 
322
 
    def set_diff(self, description, rev_tree, parent_tree):
 
326
    def set_diff(self, rev_tree, parent_tree):
323
327
        """Set the differences showed by this window.
324
328
 
325
329
        Compares the two trees and populates the window with the
326
330
        differences.
327
331
        """
 
332
        if getattr(self, 'diff_view', None) is None:
 
333
            self.diff_view = DiffView()
 
334
            self.pack2(self.diff_view)
 
335
        self.diff_view.show()
328
336
        self.diff_view.set_trees(rev_tree, parent_tree)
329
337
        self.rev_tree = rev_tree
330
338
        self.parent_tree = parent_tree
356
364
                self.model.append(titer, [ path, path ])
357
365
 
358
366
        self.treeview.expand_all()
359
 
        self.set_title(description + " - bzrk diff")
 
367
        self.diff_view.show_diff(None)
360
368
 
361
369
    def set_file(self, file_path):
 
370
        """Select the current file to display"""
362
371
        tv_path = None
363
372
        for data in self.model:
364
373
            for child in data.iterchildren():
366
375
                    tv_path = child.path
367
376
                    break
368
377
        if tv_path is None:
369
 
            raise NoSuchFile(file_path)
 
378
            raise errors.NoSuchFile(file_path)
370
379
        self.treeview.set_cursor(tv_path)
371
380
        self.treeview.scroll_to_cell(tv_path)
372
381
 
378
387
            return
379
388
        elif specific_files == [ "" ]:
380
389
            specific_files = None
381
 
 
 
390
        
382
391
        self.diff_view.show_diff(specific_files)
383
 
 
384
 
 
385
 
def _iter_changes_to_status(source, target):
 
392
    
 
393
    def _on_wraplines_toggled(self, widget=None, wrap=False):
 
394
        """Callback for when the wrap lines checkbutton is toggled"""
 
395
        if wrap or widget.get_active():
 
396
            self.diff_view.sourceview.set_wrap_mode(gtk.WRAP_WORD)
 
397
        else:
 
398
            self.diff_view.sourceview.set_wrap_mode(gtk.WRAP_NONE)
 
399
 
 
400
class DiffWindow(Window):
 
401
    """Diff window.
 
402
 
 
403
    This object represents and manages a single window containing the
 
404
    differences between two revisions on a branch.
 
405
    """
 
406
 
 
407
    def __init__(self, parent=None, operations=None):
 
408
        Window.__init__(self, parent)
 
409
        self.set_border_width(0)
 
410
        self.set_title("bzrk diff")
 
411
 
 
412
        # Use two thirds of the screen by default
 
413
        screen = self.get_screen()
 
414
        monitor = screen.get_monitor_geometry(0)
 
415
        width = int(monitor.width * 0.66)
 
416
        height = int(monitor.height * 0.66)
 
417
        self.set_default_size(width, height)
 
418
        self.construct(operations)
 
419
 
 
420
    def construct(self, operations):
 
421
        """Construct the window contents."""
 
422
        self.vbox = gtk.VBox()
 
423
        self.add(self.vbox)
 
424
        self.vbox.show()
 
425
        self.diff = DiffWidget()
 
426
        self.vbox.pack_end(self.diff, True, True, 0)
 
427
        self.diff.show_all()
 
428
        # Build after DiffWidget to connect signals
 
429
        menubar = self._get_menu_bar()
 
430
        self.vbox.pack_start(menubar, False, False, 0)
 
431
        hbox = self._get_button_bar(operations)
 
432
        if hbox is not None:
 
433
            self.vbox.pack_start(hbox, False, True, 0)
 
434
        
 
435
    
 
436
    def _get_menu_bar(self):
 
437
        menubar = gtk.MenuBar()
 
438
        # View menu
 
439
        mb_view = gtk.MenuItem(_i18n("_View"))
 
440
        mb_view_menu = gtk.Menu()
 
441
        mb_view_wrapsource = gtk.CheckMenuItem(_i18n("Wrap _Long Lines"))
 
442
        mb_view_wrapsource.connect('activate', self.diff._on_wraplines_toggled)
 
443
        mb_view_wrapsource.show()
 
444
        mb_view_menu.append(mb_view_wrapsource)
 
445
        mb_view.show()
 
446
        mb_view.set_submenu(mb_view_menu)
 
447
        mb_view.show()
 
448
        menubar.append(mb_view)
 
449
        menubar.show()
 
450
        return menubar
 
451
    
 
452
    def _get_button_bar(self, operations):
 
453
        """Return a button bar to use.
 
454
 
 
455
        :return: None, meaning that no button bar will be used.
 
456
        """
 
457
        if operations is None:
 
458
            return None
 
459
        hbox = gtk.HButtonBox()
 
460
        hbox.set_layout(gtk.BUTTONBOX_START)
 
461
        for title, method in operations:
 
462
            merge_button = gtk.Button(title)
 
463
            merge_button.show()
 
464
            merge_button.set_relief(gtk.RELIEF_NONE)
 
465
            merge_button.connect("clicked", method)
 
466
            hbox.pack_start(merge_button, expand=False, fill=True)
 
467
        hbox.show()
 
468
        return hbox
 
469
 
 
470
    def _get_merge_target(self):
 
471
        d = gtk.FileChooserDialog('Merge branch', self,
 
472
                                  gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
 
473
                                  buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
 
474
                                           gtk.STOCK_CANCEL,
 
475
                                           gtk.RESPONSE_CANCEL,))
 
476
        try:
 
477
            result = d.run()
 
478
            if result != gtk.RESPONSE_OK:
 
479
                raise SelectCancelled()
 
480
            return d.get_current_folder_uri()
 
481
        finally:
 
482
            d.destroy()
 
483
 
 
484
    def _merge_successful(self):
 
485
        # No conflicts found.
 
486
        info_dialog(_i18n('Merge successful'),
 
487
                    _i18n('All changes applied successfully.'))
 
488
 
 
489
    def _conflicts(self):
 
490
        warning_dialog(_i18n('Conflicts encountered'),
 
491
                       _i18n('Please resolve the conflicts manually'
 
492
                             ' before committing.'))
 
493
 
 
494
    def _handle_error(self, e):
 
495
        error_dialog('Error', str(e))
 
496
 
 
497
    def _get_save_path(self, basename):
 
498
        d = gtk.FileChooserDialog('Save As', self,
 
499
                                  gtk.FILE_CHOOSER_ACTION_SAVE,
 
500
                                  buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
 
501
                                           gtk.STOCK_CANCEL,
 
502
                                           gtk.RESPONSE_CANCEL,))
 
503
        d.set_current_name(basename)
 
504
        try:
 
505
            result = d.run()
 
506
            if result != gtk.RESPONSE_OK:
 
507
                raise SelectCancelled()
 
508
            return urlutils.local_path_from_url(d.get_uri())
 
509
        finally:
 
510
            d.destroy()
 
511
 
 
512
    def set_diff(self, description, rev_tree, parent_tree):
 
513
        """Set the differences showed by this window.
 
514
 
 
515
        Compares the two trees and populates the window with the
 
516
        differences.
 
517
        """
 
518
        self.diff.set_diff(rev_tree, parent_tree)
 
519
        self.set_title(description + " - bzrk diff")
 
520
 
 
521
    def set_file(self, file_path):
 
522
        self.diff.set_file(file_path)
 
523
 
 
524
 
 
525
class DiffController(object):
 
526
 
 
527
    def __init__(self, path, patch, window=None):
 
528
        self.path = path
 
529
        self.patch = patch
 
530
        if window is None:
 
531
            window = DiffWindow(operations=self._provide_operations())
 
532
            self.initialize_window(window)
 
533
        self.window = window
 
534
 
 
535
    def initialize_window(self, window):
 
536
        window.diff.set_diff_text_sections(self.get_diff_sections())
 
537
        window.set_title(self.path + " - diff")
 
538
 
 
539
    def get_diff_sections(self):
 
540
        yield "Complete Diff", None, ''.join(self.patch)
 
541
        for patch in parse_patches(self.patch):
 
542
            oldname = patch.oldname.split('\t')[0]
 
543
            newname = patch.newname.split('\t')[0]
 
544
            yield oldname, newname, str(patch)
 
545
 
 
546
    def perform_save(self, window):
 
547
        try:
 
548
            save_path = self.window._get_save_path(osutils.basename(self.path))
 
549
        except SelectCancelled:
 
550
            return
 
551
        source = open(self.path, 'rb')
 
552
        try:
 
553
            target = open(save_path, 'wb')
 
554
            try:
 
555
                osutils.pumpfile(source, target)
 
556
            finally:
 
557
                target.close()
 
558
        finally:
 
559
            source.close()
 
560
 
 
561
    def _provide_operations(self):
 
562
        return [('Save', self.perform_save)]
 
563
 
 
564
 
 
565
class MergeDirectiveController(DiffController):
 
566
 
 
567
    def __init__(self, path, directive, window=None):
 
568
        DiffController.__init__(self, path, directive.patch.splitlines(True),
 
569
                                window)
 
570
        self.directive = directive
 
571
        self.merge_target = None
 
572
 
 
573
    def _provide_operations(self):
 
574
        return [('Merge', self.perform_merge), ('Save', self.perform_save)]
 
575
 
 
576
    def perform_merge(self, window):
 
577
        if self.merge_target is None:
 
578
            try:
 
579
                self.merge_target = self.window._get_merge_target()
 
580
            except SelectCancelled:
 
581
                return
 
582
        tree = workingtree.WorkingTree.open(self.merge_target)
 
583
        tree.lock_write()
 
584
        try:
 
585
            try:
 
586
                if tree.has_changes():
 
587
                    raise errors.UncommittedChanges(tree)
 
588
                merger, verified = _mod_merge.Merger.from_mergeable(
 
589
                    tree, self.directive, pb=None)
 
590
                merger.merge_type = _mod_merge.Merge3Merger
 
591
                conflict_count = merger.do_merge()
 
592
                merger.set_pending()
 
593
                if conflict_count == 0:
 
594
                    self.window._merge_successful()
 
595
                else:
 
596
                    self.window._conflicts()
 
597
                    # There are conflicts to be resolved.
 
598
                self.window.destroy()
 
599
            except Exception, e:
 
600
                self.window._handle_error(e)
 
601
        finally:
 
602
            tree.unlock()
 
603
 
 
604
 
 
605
def iter_changes_to_status(source, target):
386
606
    """Determine the differences between trees.
387
607
 
388
 
    This is a wrapper around _iter_changes which just yields more
 
608
    This is a wrapper around iter_changes which just yields more
389
609
    understandable results.
390
610
 
391
611
    :param source: The source tree (basis tree)
398
618
    renamed_and_modified = 'renamed and modified'
399
619
    modified = 'modified'
400
620
    kind_changed = 'kind changed'
 
621
    missing = 'missing'
401
622
 
402
623
    # TODO: Handle metadata changes
403
624
 
407
628
        source.lock_read()
408
629
        try:
409
630
            for (file_id, paths, changed_content, versioned, parent_ids, names,
410
 
                 kinds, executables) in target._iter_changes(source):
 
631
                 kinds, executables) in target.iter_changes(source):
411
632
 
412
633
                # Skip the root entry if it isn't very interesting
413
634
                if parent_ids == (None, None):
418
639
                    source_marker = ''
419
640
                else:
420
641
                    source_marker = osutils.kind_marker(kinds[0])
 
642
 
421
643
                if kinds[1] is None:
422
 
                    assert kinds[0] is not None
423
 
                    marker = osutils.kind_marker(kinds[0])
 
644
                    if kinds[0] is None:
 
645
                        # We assume bzr will flag only files in that case,
 
646
                        # there may be a bzr bug there as only files seems to
 
647
                        # not receive any kind.
 
648
                        marker = osutils.kind_marker('file')
 
649
                    else:
 
650
                        marker = osutils.kind_marker(kinds[0])
424
651
                else:
425
652
                    marker = osutils.kind_marker(kinds[1])
426
653
 
428
655
                if real_path is None:
429
656
                    real_path = paths[0]
430
657
                assert real_path is not None
431
 
                display_path = real_path + marker
432
658
 
433
659
                present_source = versioned[0] and kinds[0] is not None
434
660
                present_target = versioned[1] and kinds[1] is not None
435
661
 
436
 
                if present_source != present_target:
 
662
                if kinds[0] is None and kinds[1] is None:
 
663
                    change_type = missing
 
664
                    display_path = real_path + marker
 
665
                elif present_source != present_target:
437
666
                    if present_target:
438
667
                        change_type = added
439
668
                    else:
440
669
                        assert present_source
441
670
                        change_type = removed
 
671
                    display_path = real_path + marker
442
672
                elif names[0] != names[1] or parent_ids[0] != parent_ids[1]:
443
673
                    # Renamed
444
674
                    if changed_content or executables[0] != executables[1]:
452
682
                    change_type = kind_changed
453
683
                    display_path = (paths[0] + source_marker
454
684
                                    + ' => ' + paths[1] + marker)
455
 
                elif changed_content is True or executables[0] != executables[1]:
 
685
                elif changed_content or executables[0] != executables[1]:
456
686
                    change_type = modified
 
687
                    display_path = real_path + marker
457
688
                else:
458
689
                    assert False, "How did we get here?"
459
690