3
This module contains the code to manage the diff window which shows
4
the changes made between two revisions on a branch.
7
__copyright__ = "Copyright 2005 Canonical Ltd."
8
__author__ = "Scott James Remnant <scott@ubuntu.com>"
11
from cStringIO import StringIO
13
from gi.repository import Gtk
14
from gi.repository import Pango
20
from xml.etree.ElementTree import Element, SubElement, tostring
22
from elementtree.ElementTree import Element, SubElement, tostring
25
from gi.repository import GtkSource
26
have_gtksourceview = True
28
have_gtksourceview = False
30
from gi.repository import GConf
42
from bzrlib.diff import show_diff_trees
43
from bzrlib.patches import parse_patches
44
from bzrlib.trace import warning
45
from bzrlib.plugins.gtk.dialog import (
50
from bzrlib.plugins.gtk.i18n import _i18n
51
from bzrlib.plugins.gtk.window import Window
54
def fallback_guess_language(slm, content_type):
55
for lang_id in slm.get_language_ids():
56
lang = slm.get_language(lang_id)
57
if "text/x-patch" in lang.get_mime_types():
62
class SelectCancelled(Exception):
67
class DiffFileView(Gtk.ScrolledWindow):
68
"""Window for displaying diffs from a diff file"""
71
GObject.GObject.__init__(self)
76
self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
77
self.set_shadow_type(Gtk.ShadowType.IN)
79
if have_gtksourceview:
80
self.buffer = GtkSource.Buffer()
81
slm = GtkSource.LanguageManager()
82
guess_language = getattr(GtkSource.LanguageManager,
83
"guess_language", fallback_guess_language)
84
gsl = guess_language(slm, content_type="text/x-patch")
86
self.apply_gedit_colors(self.buffer)
87
self.apply_colordiff_colors(self.buffer)
88
self.buffer.set_language(gsl)
89
self.buffer.set_highlight_syntax(True)
91
self.sourceview = GtkSource.View(self.buffer)
93
self.buffer = Gtk.TextBuffer()
94
self.sourceview = Gtk.TextView(self.buffer)
96
self.sourceview.set_editable(False)
97
self.sourceview.modify_font(Pango.FontDescription("Monospace"))
98
self.add(self.sourceview)
99
self.sourceview.show()
102
def apply_gedit_colors(buf):
103
"""Set style to that specified in gedit configuration.
105
This method needs the gconf module.
107
:param buf: a GtkSource.Buffer object.
109
GEDIT_SCHEME_PATH = '/apps/gedit-2/preferences/editor/colors/scheme'
110
GEDIT_USER_STYLES_PATH = os.path.expanduser('~/.gnome2/gedit/styles')
112
client = GConf.Client.get_default()
113
style_scheme_name = client.get_string(GEDIT_SCHEME_PATH)
114
if style_scheme_name is not None:
115
style_scheme_mgr = GtkSource.StyleSchemeManager()
116
style_scheme_mgr.append_search_path(GEDIT_USER_STYLES_PATH)
118
style_scheme = style_scheme_mgr.get_scheme(style_scheme_name)
120
if style_scheme is not None:
121
buf.set_style_scheme(style_scheme)
124
def apply_colordiff_colors(klass, buf):
125
"""Set style colors for lang using the colordiff configuration file.
127
Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
129
:param buf: a "Diff" GtkSource.Buffer object.
131
scheme_manager = GtkSource.StyleSchemeManager()
132
style_scheme = scheme_manager.get_scheme('colordiff')
134
# if style scheme not found, we'll generate it from colordiffrc
135
# TODO: reload if colordiffrc has changed.
136
if style_scheme is None:
139
for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
140
f = os.path.expanduser(f)
141
if os.path.exists(f):
145
warning('could not open file %s: %s' % (f, str(e)))
147
colors.update(klass.parse_colordiffrc(f))
151
# ~/.colordiffrc does not exist
155
# map GtkSourceView2 scheme styles to colordiff names
156
# since GSV is richer, accept new names for extra bits,
157
# defaulting to old names if they're not present
158
'diff:added-line': ['newtext'],
159
'diff:removed-line': ['oldtext'],
160
'diff:location': ['location', 'diffstuff'],
161
'diff:file': ['file', 'diffstuff'],
162
'diff:special-case': ['specialcase', 'diffstuff'],
165
converted_colors = {}
166
for name, values in mapping.items():
169
color = colors.get(value, None)
170
if color is not None:
174
converted_colors[name] = color
176
# some xml magic to produce needed style scheme description
177
e_style_scheme = Element('style-scheme')
178
e_style_scheme.set('id', 'colordiff')
179
e_style_scheme.set('_name', 'ColorDiff')
180
e_style_scheme.set('version', '1.0')
181
for name, color in converted_colors.items():
182
style = SubElement(e_style_scheme, 'style')
183
style.set('name', name)
184
style.set('foreground', '#%s' % color)
186
scheme_xml = tostring(e_style_scheme, 'UTF-8')
187
if not os.path.exists(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles')):
188
os.makedirs(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles'))
189
file(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles/colordiff.xml'), 'w').write(scheme_xml)
191
scheme_manager.force_rescan()
192
style_scheme = scheme_manager.get_scheme('colordiff')
194
buf.set_style_scheme(style_scheme)
197
def parse_colordiffrc(fileobj):
198
"""Parse fileobj as a colordiff configuration file.
200
:return: A dict with the key -> value pairs.
204
if re.match(r'^\s*#', line):
208
key, val = line.split('=', 1)
209
colors[key.strip()] = val.strip()
212
def set_trees(self, rev_tree, parent_tree):
213
self.rev_tree = rev_tree
214
self.parent_tree = parent_tree
215
# self._build_delta()
217
# def _build_delta(self):
218
# self.parent_tree.lock_read()
219
# self.rev_tree.lock_read()
221
# self.delta = iter_changes_to_status(self.parent_tree, self.rev_tree)
222
# self.path_to_status = {}
223
# self.path_to_diff = {}
224
# source_inv = self.parent_tree.inventory
225
# target_inv = self.rev_tree.inventory
226
# for (file_id, real_path, change_type, display_path) in self.delta:
227
# self.path_to_status[real_path] = u'=== %s %s' % (change_type, display_path)
228
# if change_type in ('modified', 'renamed and modified'):
229
# source_ie = source_inv[file_id]
230
# target_ie = target_inv[file_id]
232
# source_ie.diff(internal_diff, *old path, *old_tree,
233
# *new_path, target_ie, self.rev_tree,
235
# self.path_to_diff[real_path] =
238
# self.rev_tree.unlock()
239
# self.parent_tree.unlock()
241
def show_diff(self, specific_files):
243
if specific_files is None:
244
self.buffer.set_text(self._diffs[None])
246
for specific_file in specific_files:
247
sections.append(self._diffs[specific_file])
248
self.buffer.set_text(''.join(sections))
251
class DiffView(DiffFileView):
252
"""This is the soft and chewy filling for a DiffWindow."""
255
DiffFileView.__init__(self)
257
self.parent_tree = None
259
def show_diff(self, specific_files):
260
"""Show the diff for the specified files"""
262
show_diff_trees(self.parent_tree, self.rev_tree, s, specific_files,
263
old_label='', new_label='',
264
# path_encoding=sys.getdefaultencoding()
265
# The default is utf-8, but we interpret the file
266
# contents as getdefaultencoding(), so we should
267
# probably try to make the paths in the same encoding.
269
# str.decode(encoding, 'replace') doesn't do anything. Because if a
270
# character is not valid in 'encoding' there is nothing to replace, the
271
# 'replace' is for 'str.encode()'
273
decoded = s.getvalue().decode(sys.getdefaultencoding())
274
except UnicodeDecodeError:
276
decoded = s.getvalue().decode('UTF-8')
277
except UnicodeDecodeError:
278
decoded = s.getvalue().decode('iso-8859-1')
279
# This always works, because every byte has a valid
280
# mapping from iso-8859-1 to Unicode
281
# TextBuffer must contain pure UTF-8 data
282
self.buffer.set_text(decoded.encode('UTF-8'))
285
class DiffWidget(Gtk.HPaned):
290
super(DiffWidget, self).__init__()
292
# The file hierarchy: a scrollable treeview
293
scrollwin = Gtk.ScrolledWindow()
294
scrollwin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
295
scrollwin.set_shadow_type(Gtk.ShadowType.IN)
296
self.pack1(scrollwin)
299
self.model = Gtk.TreeStore(str, str)
300
self.treeview = Gtk.TreeView(self.model)
301
self.treeview.set_headers_visible(False)
302
self.treeview.set_search_column(1)
303
self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
304
scrollwin.add(self.treeview)
307
cell = Gtk.CellRendererText()
308
cell.set_property("width-chars", 20)
309
column = Gtk.TreeViewColumn()
310
column.pack_start(cell, True, True, 0)
311
column.add_attribute(cell, "text", 0)
312
self.treeview.append_column(column)
314
def set_diff_text(self, lines):
315
"""Set the current diff from a list of lines
317
:param lines: The diff to show, in unified diff format
319
# The diffs of the selected file: a scrollable source or
322
def set_diff_text_sections(self, sections):
323
if getattr(self, 'diff_view', None) is None:
324
self.diff_view = DiffFileView()
325
self.pack2(self.diff_view)
326
self.diff_view.show()
327
for oldname, newname, patch in sections:
328
self.diff_view._diffs[newname] = str(patch)
331
self.model.append(None, [oldname, newname])
332
self.diff_view.show_diff(None)
334
def set_diff(self, rev_tree, parent_tree):
335
"""Set the differences showed by this window.
337
Compares the two trees and populates the window with the
340
if getattr(self, 'diff_view', None) is None:
341
self.diff_view = DiffView()
342
self.pack2(self.diff_view)
343
self.diff_view.show()
344
self.diff_view.set_trees(rev_tree, parent_tree)
345
self.rev_tree = rev_tree
346
self.parent_tree = parent_tree
349
delta = self.rev_tree.changes_from(self.parent_tree)
351
self.model.append(None, [ "Complete Diff", "" ])
354
titer = self.model.append(None, [ "Added", None ])
355
for path, id, kind in delta.added:
356
self.model.append(titer, [ path, path ])
358
if len(delta.removed):
359
titer = self.model.append(None, [ "Removed", None ])
360
for path, id, kind in delta.removed:
361
self.model.append(titer, [ path, path ])
363
if len(delta.renamed):
364
titer = self.model.append(None, [ "Renamed", None ])
365
for oldpath, newpath, id, kind, text_modified, meta_modified \
367
self.model.append(titer, [ oldpath, newpath ])
369
if len(delta.modified):
370
titer = self.model.append(None, [ "Modified", None ])
371
for path, id, kind, text_modified, meta_modified in delta.modified:
372
self.model.append(titer, [ path, path ])
374
self.treeview.expand_all()
375
self.diff_view.show_diff(None)
377
def set_file(self, file_path):
378
"""Select the current file to display"""
380
for data in self.model:
381
for child in data.iterchildren():
382
if child[0] == file_path or child[1] == file_path:
386
raise errors.NoSuchFile(file_path)
387
self.treeview.set_cursor(tv_path)
388
self.treeview.scroll_to_cell(tv_path)
390
def _treeview_cursor_cb(self, *args):
391
"""Callback for when the treeview cursor changes."""
392
(path, col) = self.treeview.get_cursor()
393
specific_files = [ self.model[path][1] ]
394
if specific_files == [ None ]:
396
elif specific_files == [ "" ]:
397
specific_files = None
399
self.diff_view.show_diff(specific_files)
401
def _on_wraplines_toggled(self, widget=None, wrap=False):
402
"""Callback for when the wrap lines checkbutton is toggled"""
403
if wrap or widget.get_active():
404
self.diff_view.sourceview.set_wrap_mode(Gtk.WrapMode.WORD)
406
self.diff_view.sourceview.set_wrap_mode(Gtk.WrapMode.NONE)
408
class DiffWindow(Window):
411
This object represents and manages a single window containing the
412
differences between two revisions on a branch.
415
def __init__(self, parent=None, operations=None):
416
Window.__init__(self, parent)
417
self.set_border_width(0)
418
self.set_title("bzrk diff")
420
# Use two thirds of the screen by default
421
screen = self.get_screen()
422
monitor = screen.get_monitor_geometry(0)
423
width = int(monitor.width * 0.66)
424
height = int(monitor.height * 0.66)
425
self.set_default_size(width, height)
426
self.construct(operations)
428
def construct(self, operations):
429
"""Construct the window contents."""
430
self.vbox = Gtk.VBox()
433
self.diff = DiffWidget()
434
self.vbox.pack_end(self.diff, True, True, 0)
436
# Build after DiffWidget to connect signals
437
menubar = self._get_menu_bar()
438
self.vbox.pack_start(menubar, False, False, 0)
439
hbox = self._get_button_bar(operations)
441
self.vbox.pack_start(hbox, False, True, 0)
444
def _get_menu_bar(self):
445
menubar = Gtk.MenuBar()
447
mb_view = Gtk.MenuItem(_i18n("_View"))
448
mb_view_menu = Gtk.Menu()
449
mb_view_wrapsource = Gtk.CheckMenuItem(_i18n("Wrap _Long Lines"))
450
mb_view_wrapsource.connect('activate', self.diff._on_wraplines_toggled)
451
mb_view_wrapsource.show()
452
mb_view_menu.append(mb_view_wrapsource)
454
mb_view.set_submenu(mb_view_menu)
456
menubar.append(mb_view)
460
def _get_button_bar(self, operations):
461
"""Return a button bar to use.
463
:return: None, meaning that no button bar will be used.
465
if operations is None:
467
hbox = Gtk.HButtonBox()
468
hbox.set_layout(Gtk.ButtonBoxStyle.START)
469
for title, method in operations:
470
merge_button = Gtk.Button(title)
472
merge_button.set_relief(Gtk.ReliefStyle.NONE)
473
merge_button.connect("clicked", method)
474
hbox.pack_start(merge_button, expand=False, fill=True)
478
def _get_merge_target(self):
479
d = Gtk.FileChooserDialog('Merge branch', self,
480
Gtk.FileChooserAction.SELECT_FOLDER,
481
buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK,
483
Gtk.ResponseType.CANCEL,))
486
if result != Gtk.ResponseType.OK:
487
raise SelectCancelled()
488
return d.get_current_folder_uri()
492
def _merge_successful(self):
493
# No conflicts found.
494
info_dialog(_i18n('Merge successful'),
495
_i18n('All changes applied successfully.'))
497
def _conflicts(self):
498
warning_dialog(_i18n('Conflicts encountered'),
499
_i18n('Please resolve the conflicts manually'
500
' before committing.'))
502
def _handle_error(self, e):
503
error_dialog('Error', str(e))
505
def _get_save_path(self, basename):
506
d = Gtk.FileChooserDialog('Save As', self,
507
Gtk.FileChooserAction.SAVE,
508
buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK,
510
Gtk.ResponseType.CANCEL,))
511
d.set_current_name(basename)
514
if result != Gtk.ResponseType.OK:
515
raise SelectCancelled()
516
return urlutils.local_path_from_url(d.get_uri())
520
def set_diff(self, description, rev_tree, parent_tree):
521
"""Set the differences showed by this window.
523
Compares the two trees and populates the window with the
526
self.diff.set_diff(rev_tree, parent_tree)
527
self.set_title(description + " - bzrk diff")
529
def set_file(self, file_path):
530
self.diff.set_file(file_path)
533
class DiffController(object):
535
def __init__(self, path, patch, window=None, allow_dirty=False):
538
self.allow_dirty = allow_dirty
540
window = DiffWindow(operations=self._provide_operations())
541
self.initialize_window(window)
544
def initialize_window(self, window):
545
window.diff.set_diff_text_sections(self.get_diff_sections())
546
window.set_title(self.path + " - diff")
548
def get_diff_sections(self):
549
yield "Complete Diff", None, ''.join(self.patch)
550
# allow_dirty was added to parse_patches in bzrlib 2.2b1
551
if 'allow_dirty' in inspect.getargspec(parse_patches).args:
552
patches = parse_patches(self.patch, allow_dirty=self.allow_dirty)
554
patches = parse_patches(self.patch)
555
for patch in patches:
556
oldname = patch.oldname.split('\t')[0]
557
newname = patch.newname.split('\t')[0]
558
yield oldname, newname, str(patch)
560
def perform_save(self, window):
562
save_path = self.window._get_save_path(osutils.basename(self.path))
563
except SelectCancelled:
565
source = open(self.path, 'rb')
567
target = open(save_path, 'wb')
569
osutils.pumpfile(source, target)
575
def _provide_operations(self):
576
return [('Save', self.perform_save)]
579
class MergeDirectiveController(DiffController):
581
def __init__(self, path, directive, window=None):
582
DiffController.__init__(self, path, directive.patch.splitlines(True),
584
self.directive = directive
585
self.merge_target = None
587
def _provide_operations(self):
588
return [('Merge', self.perform_merge), ('Save', self.perform_save)]
590
def perform_merge(self, window):
591
if self.merge_target is None:
593
self.merge_target = self.window._get_merge_target()
594
except SelectCancelled:
596
tree = workingtree.WorkingTree.open(self.merge_target)
600
if tree.has_changes():
601
raise errors.UncommittedChanges(tree)
602
merger, verified = _mod_merge.Merger.from_mergeable(
603
tree, self.directive, pb=None)
604
merger.merge_type = _mod_merge.Merge3Merger
605
conflict_count = merger.do_merge()
607
if conflict_count == 0:
608
self.window._merge_successful()
610
self.window._conflicts()
611
# There are conflicts to be resolved.
612
self.window.destroy()
614
self.window._handle_error(e)
619
def iter_changes_to_status(source, target):
620
"""Determine the differences between trees.
622
This is a wrapper around iter_changes which just yields more
623
understandable results.
625
:param source: The source tree (basis tree)
626
:param target: The target tree
627
:return: A list of (file_id, real_path, change_type, display_path)
632
renamed_and_modified = 'renamed and modified'
633
modified = 'modified'
634
kind_changed = 'kind changed'
637
# TODO: Handle metadata changes
644
for (file_id, paths, changed_content, versioned, parent_ids, names,
645
kinds, executables) in target.iter_changes(source):
647
# Skip the root entry if it isn't very interesting
648
if parent_ids == (None, None):
655
source_marker = osutils.kind_marker(kinds[0])
659
# We assume bzr will flag only files in that case,
660
# there may be a bzr bug there as only files seems to
661
# not receive any kind.
662
marker = osutils.kind_marker('file')
664
marker = osutils.kind_marker(kinds[0])
666
marker = osutils.kind_marker(kinds[1])
669
if real_path is None:
671
assert real_path is not None
673
present_source = versioned[0] and kinds[0] is not None
674
present_target = versioned[1] and kinds[1] is not None
676
if kinds[0] is None and kinds[1] is None:
677
change_type = missing
678
display_path = real_path + marker
679
elif present_source != present_target:
683
assert present_source
684
change_type = removed
685
display_path = real_path + marker
686
elif names[0] != names[1] or parent_ids[0] != parent_ids[1]:
688
if changed_content or executables[0] != executables[1]:
690
change_type = renamed_and_modified
692
change_type = renamed
693
display_path = (paths[0] + source_marker
694
+ ' => ' + paths[1] + marker)
695
elif kinds[0] != kinds[1]:
696
change_type = kind_changed
697
display_path = (paths[0] + source_marker
698
+ ' => ' + paths[1] + marker)
699
elif changed_content or executables[0] != executables[1]:
700
change_type = modified
701
display_path = real_path + marker
703
assert False, "How did we get here?"
705
status.append((file_id, real_path, change_type, display_path))