1
# -*- coding: UTF-8 -*-
1
2
"""Difference window.
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.
7
__copyright__ = "Copyright 2005 Canonical Ltd."
8
__copyright__ = "Copyright © 2005 Canonical Ltd."
8
9
__author__ = "Scott James Remnant <scott@ubuntu.com>"
11
12
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
24
have_gtksourceview = True
27
25
except ImportError:
28
26
have_gtksourceview = False
30
from gi.repository import GConf
32
30
except ImportError:
42
from bzrlib.diff import show_diff_trees
43
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
44
36
from bzrlib.trace import warning
45
from bzrlib.plugins.gtk.dialog import (
50
from bzrlib.plugins.gtk.i18n import _i18n
51
37
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"""
40
class DiffView(gtk.ScrolledWindow):
41
"""This is the soft and chewy filling for a DiffWindow."""
70
43
def __init__(self):
71
Gtk.ScrolledWindow.__init__(self)
44
gtk.ScrolledWindow.__init__(self)
48
self.parent_tree = None
75
50
def construct(self):
76
self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
77
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)
79
54
if have_gtksourceview:
80
self.buffer = GtkSource.Buffer()
81
lang_manager = GtkSource.LanguageManager.get_default()
82
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")
84
self.apply_gedit_colors(self.buffer)
85
self.apply_colordiff_colors(self.buffer)
86
self.buffer.set_language(language)
87
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)
89
self.sourceview = GtkSource.View(buffer=self.buffer)
64
sourceview = gtksourceview.SourceView(self.buffer)
91
self.buffer = Gtk.TextBuffer()
92
self.sourceview = Gtk.TextView(self.buffer)
66
self.buffer = gtk.TextBuffer()
67
sourceview = gtk.TextView(self.buffer)
94
self.sourceview.set_editable(False)
95
self.sourceview.modify_font(Pango.FontDescription("Monospace"))
96
self.add(self.sourceview)
97
self.sourceview.show()
69
sourceview.set_editable(False)
70
sourceview.modify_font(pango.FontDescription("Monospace"))
100
def apply_gedit_colors(buf):
101
"""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.
103
78
This method needs the gconf module.
105
:param buf: a GtkSource.Buffer object.
80
:param lang: a gtksourceview.SourceLanguage object.
107
GEDIT_SCHEME_PATH = '/apps/gedit-2/preferences/editor/colors/scheme'
108
GEDIT_USER_STYLES_PATH = os.path.expanduser('~/.gnome2/gedit/styles')
110
client = GConf.Client.get_default()
111
style_scheme_name = client.get_string(GEDIT_SCHEME_PATH)
112
if style_scheme_name is not None:
113
style_scheme_mgr = GtkSource.StyleSchemeManager()
114
style_scheme_mgr.append_search_path(GEDIT_USER_STYLES_PATH)
116
style_scheme = style_scheme_mgr.get_scheme(style_scheme_name)
118
if style_scheme is not None:
119
buf.set_style_scheme(style_scheme)
122
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()
85
client = gconf.client_get_default()
86
client.add_dir(GEDIT_LANG_PATH, gconf.CLIENT_PRELOAD_NONE)
88
for tag in lang.get_tags():
90
gconf_key = GEDIT_LANG_PATH + '/' + tag_id
91
style_string = client.get_string(gconf_key)
93
if style_string is None:
96
# function to get a bool from a string that's either '0' or '1'
97
string_bool = lambda x: bool(int(x))
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 ]
108
style = gtksourceview.SourceTagStyle()
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
122
if not (mask & 2): # GTK_SOURCE_TAG_STYLE_USE_FOREGROUND
124
items[0:1] = [] # skip the mask unconditionally
126
for value, attr, func in items:
130
warning('gconf key %s contains an invalid value: %s'
133
setattr(style, attr, value)
135
lang.set_tag_style(tag_id, style)
138
def apply_colordiff_colors(lang):
123
139
"""Set style colors for lang using the colordiff configuration file.
125
141
Both ~/.colordiffrc and ~/.colordiffrc.bzr-gtk are read.
127
:param buf: a "Diff" GtkSource.Buffer object.
143
:param lang: a "Diff" gtksourceview.SourceLanguage object.
129
scheme_manager = GtkSource.StyleSchemeManager()
130
style_scheme = scheme_manager.get_scheme('colordiff')
132
# if style scheme not found, we'll generate it from colordiffrc
133
# TODO: reload if colordiffrc has changed.
134
if style_scheme is None:
137
for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
138
f = os.path.expanduser(f)
139
if os.path.exists(f):
143
warning('could not open file %s: %s' % (f, str(e)))
145
colors.update(klass.parse_colordiffrc(f))
149
# ~/.colordiffrc does not exist
153
# map GtkSourceView2 scheme styles to colordiff names
147
for f in ('~/.colordiffrc', '~/.colordiffrc.bzr-gtk'):
148
f = os.path.expanduser(f)
149
if os.path.exists(f):
153
warning('could not open file %s: %s' % (f, str(e)))
155
colors.update(DiffView.parse_colordiffrc(f))
159
# ~/.colordiffrc does not exist
163
# map GtkSourceView tags to colordiff names
154
164
# since GSV is richer, accept new names for extra bits,
155
165
# defaulting to old names if they're not present
156
'diff:added-line': ['newtext'],
157
'diff:removed-line': ['oldtext'],
158
'diff:location': ['location', 'diffstuff'],
159
'diff:file': ['file', 'diffstuff'],
160
'diff:special-case': ['specialcase', 'diffstuff'],
163
converted_colors = {}
164
for name, values in mapping.items():
167
color = colors.get(value, None)
168
if color is not None:
172
converted_colors[name] = color
174
# some xml magic to produce needed style scheme description
175
e_style_scheme = Element('style-scheme')
176
e_style_scheme.set('id', 'colordiff')
177
e_style_scheme.set('_name', 'ColorDiff')
178
e_style_scheme.set('version', '1.0')
179
for name, color in converted_colors.items():
180
style = SubElement(e_style_scheme, 'style')
181
style.set('name', name)
182
style.set('foreground', '#%s' % color)
184
scheme_xml = tostring(e_style_scheme, 'UTF-8')
185
if not os.path.exists(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles')):
186
os.makedirs(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles'))
187
file(os.path.expanduser('~/.local/share/gtksourceview-2.0/styles/colordiff.xml'), 'w').write(scheme_xml)
189
scheme_manager.force_rescan()
190
style_scheme = scheme_manager.get_scheme('colordiff')
192
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'],
173
for tag in lang.get_tags():
174
tag_id = tag.get_id()
175
keys = mapping.get(tag_id, [])
179
color = colors.get(key, None)
180
if color is not None:
186
style = gtksourceview.SourceTagStyle()
188
style.foreground = gtk.gdk.color_parse(color)
190
warning('not a valid color: %s' % color)
192
lang.set_tag_style(tag_id, style)
195
195
def parse_colordiffrc(fileobj):
280
261
self.buffer.set_text(decoded.encode('UTF-8'))
283
class DiffWidget(Gtk.HPaned):
264
class DiffWindow(Window):
267
This object represents and manages a single window containing the
268
differences between two revisions on a branch.
288
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")
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)
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
294
# The file hierarchy: a scrollable treeview
291
scrollwin = Gtk.ScrolledWindow()
292
scrollwin.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
293
scrollwin.set_shadow_type(Gtk.ShadowType.IN)
294
self.pack1(scrollwin)
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)
297
self.model = Gtk.TreeStore(str, str)
298
self.treeview = Gtk.TreeView(model=self.model)
301
self.model = gtk.TreeStore(str, str)
302
self.treeview = gtk.TreeView(self.model)
299
303
self.treeview.set_headers_visible(False)
300
304
self.treeview.set_search_column(1)
301
305
self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
302
306
scrollwin.add(self.treeview)
303
307
self.treeview.show()
305
cell = Gtk.CellRendererText()
309
cell = gtk.CellRendererText()
306
310
cell.set_property("width-chars", 20)
307
column = Gtk.TreeViewColumn()
308
column.pack_start(cell, True)
311
column = gtk.TreeViewColumn()
312
column.pack_start(cell, expand=True)
309
313
column.add_attribute(cell, "text", 0)
310
314
self.treeview.append_column(column)
312
def set_diff_text(self, lines):
313
"""Set the current diff from a list of lines
315
:param lines: The diff to show, in unified diff format
317
316
# The diffs of the selected file: a scrollable source or
320
def set_diff_text_sections(self, sections):
321
if getattr(self, 'diff_view', None) is None:
322
self.diff_view = DiffFileView()
323
self.pack2(self.diff_view)
318
self.diff_view = DiffView()
319
pane.pack2(self.diff_view)
324
320
self.diff_view.show()
325
for oldname, newname, patch in sections:
326
self.diff_view._diffs[newname] = str(patch)
329
self.model.append(None, [oldname, newname])
330
self.diff_view.show_diff(None)
332
def set_diff(self, rev_tree, parent_tree):
322
def set_diff(self, description, rev_tree, parent_tree):
333
323
"""Set the differences showed by this window.
335
325
Compares the two trees and populates the window with the
338
if getattr(self, 'diff_view', None) is None:
339
self.diff_view = DiffView()
340
self.pack2(self.diff_view)
341
self.diff_view.show()
342
328
self.diff_view.set_trees(rev_tree, parent_tree)
343
329
self.rev_tree = rev_tree
344
330
self.parent_tree = parent_tree
394
379
elif specific_files == [ "" ]:
395
380
specific_files = None
397
382
self.diff_view.show_diff(specific_files)
399
def _on_wraplines_toggled(self, widget=None, wrap=False):
400
"""Callback for when the wrap lines checkbutton is toggled"""
401
if wrap or widget.get_active():
402
self.diff_view.sourceview.set_wrap_mode(Gtk.WrapMode.WORD)
404
self.diff_view.sourceview.set_wrap_mode(Gtk.WrapMode.NONE)
406
class DiffWindow(Window):
409
This object represents and manages a single window containing the
410
differences between two revisions on a branch.
413
def __init__(self, parent=None, operations=None):
414
Window.__init__(self, parent)
415
self.set_border_width(0)
416
self.set_title("bzrk diff")
418
# Use two thirds of the screen by default
419
screen = self.get_screen()
420
monitor = screen.get_monitor_geometry(0)
421
width = int(monitor.width * 0.66)
422
height = int(monitor.height * 0.66)
423
self.set_default_size(width, height)
424
self.construct(operations)
426
def construct(self, operations):
427
"""Construct the window contents."""
428
self.vbox = Gtk.VBox()
431
self.diff = DiffWidget()
432
self.vbox.pack_end(self.diff, True, True, 0)
434
# Build after DiffWidget to connect signals
435
menubar = self._get_menu_bar()
436
self.vbox.pack_start(menubar, False, False, 0)
437
hbox = self._get_button_bar(operations)
439
self.vbox.pack_start(hbox, False, True, 0)
442
def _get_menu_bar(self):
443
menubar = Gtk.MenuBar()
445
mb_view = Gtk.MenuItem(label=_i18n("_View"))
446
mb_view_menu = Gtk.Menu()
447
mb_view_wrapsource = Gtk.CheckMenuItem(
448
label=_i18n("Wrap _Long Lines"))
449
mb_view_wrapsource.connect('activate', self.diff._on_wraplines_toggled)
450
mb_view_wrapsource.show()
451
mb_view_menu.append(mb_view_wrapsource)
453
mb_view.set_submenu(mb_view_menu)
455
menubar.append(mb_view)
459
def _get_button_bar(self, operations):
460
"""Return a button bar to use.
462
:return: None, meaning that no button bar will be used.
464
if operations is None:
466
hbox = Gtk.HButtonBox()
467
hbox.set_layout(Gtk.ButtonBoxStyle.START)
468
for title, method in operations:
469
merge_button = Gtk.Button(title)
471
merge_button.set_relief(Gtk.ReliefStyle.NONE)
472
merge_button.connect("clicked", method)
473
hbox.pack_start(merge_button, expand=False, fill=True)
477
def _get_merge_target(self):
478
d = Gtk.FileChooserDialog('Merge branch', self,
479
Gtk.FileChooserAction.SELECT_FOLDER,
480
buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK,
482
Gtk.ResponseType.CANCEL,))
485
if result != Gtk.ResponseType.OK:
486
raise SelectCancelled()
487
return d.get_current_folder_uri()
491
def _merge_successful(self):
492
# No conflicts found.
493
info_dialog(_i18n('Merge successful'),
494
_i18n('All changes applied successfully.'))
496
def _conflicts(self):
497
warning_dialog(_i18n('Conflicts encountered'),
498
_i18n('Please resolve the conflicts manually'
499
' before committing.'))
501
def _handle_error(self, e):
502
error_dialog('Error', str(e))
504
def _get_save_path(self, basename):
505
d = Gtk.FileChooserDialog('Save As', self,
506
Gtk.FileChooserAction.SAVE,
507
buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK,
509
Gtk.ResponseType.CANCEL,))
510
d.set_current_name(basename)
513
if result != Gtk.ResponseType.OK:
514
raise SelectCancelled()
515
return urlutils.local_path_from_url(d.get_uri())
519
def set_diff(self, description, rev_tree, parent_tree):
520
"""Set the differences showed by this window.
522
Compares the two trees and populates the window with the
525
self.diff.set_diff(rev_tree, parent_tree)
526
self.set_title(description + " - bzrk diff")
528
def set_file(self, file_path):
529
self.diff.set_file(file_path)
532
class DiffController(object):
534
def __init__(self, path, patch, window=None, allow_dirty=False):
537
self.allow_dirty = allow_dirty
539
window = DiffWindow(operations=self._provide_operations())
540
self.initialize_window(window)
543
def initialize_window(self, window):
544
window.diff.set_diff_text_sections(self.get_diff_sections())
545
window.set_title(self.path + " - diff")
547
def get_diff_sections(self):
548
yield "Complete Diff", None, ''.join(self.patch)
549
# allow_dirty was added to parse_patches in bzrlib 2.2b1
550
if 'allow_dirty' in inspect.getargspec(parse_patches).args:
551
patches = parse_patches(self.patch, allow_dirty=self.allow_dirty)
553
patches = parse_patches(self.patch)
554
for patch in patches:
555
oldname = patch.oldname.split('\t')[0]
556
newname = patch.newname.split('\t')[0]
557
yield oldname, newname, str(patch)
559
def perform_save(self, window):
561
save_path = self.window._get_save_path(osutils.basename(self.path))
562
except SelectCancelled:
564
source = open(self.path, 'rb')
566
target = open(save_path, 'wb')
568
osutils.pumpfile(source, target)
574
def _provide_operations(self):
575
return [('Save', self.perform_save)]
578
class MergeDirectiveController(DiffController):
580
def __init__(self, path, directive, window=None):
581
DiffController.__init__(self, path, directive.patch.splitlines(True),
583
self.directive = directive
584
self.merge_target = None
586
def _provide_operations(self):
587
return [('Merge', self.perform_merge), ('Save', self.perform_save)]
589
def perform_merge(self, window):
590
if self.merge_target is None:
592
self.merge_target = self.window._get_merge_target()
593
except SelectCancelled:
595
tree = workingtree.WorkingTree.open(self.merge_target)
599
if tree.has_changes():
600
raise errors.UncommittedChanges(tree)
601
merger, verified = _mod_merge.Merger.from_mergeable(
602
tree, self.directive, pb=None)
603
merger.merge_type = _mod_merge.Merge3Merger
604
conflict_count = merger.do_merge()
606
if conflict_count == 0:
607
self.window._merge_successful()
609
self.window._conflicts()
610
# There are conflicts to be resolved.
611
self.window.destroy()
613
self.window._handle_error(e)
618
def iter_changes_to_status(source, target):
385
def _iter_changes_to_status(source, target):
619
386
"""Determine the differences between trees.
621
This is a wrapper around iter_changes which just yields more
388
This is a wrapper around _iter_changes which just yields more
622
389
understandable results.
624
391
:param source: The source tree (basis tree)