/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: David Planella
  • Date: 2010-08-21 09:38:06 UTC
  • mto: This revision was merged to the branch mainline in revision 719.
  • Revision ID: david.planella@ubuntu.com-20100821093806-9u7cq5gcaml6k2ln
Added the rest of files necessary for i18n support in the build system

Show diffs side-by-side

added added

removed removed

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