1
# -*- coding: utf-8 -*-
3
# QBzr - Qt frontend to Bazaar commands
4
# Copyright (C) 2006 Lukáš Lalinský <lalinsky@gmail.com>
5
# Portions Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
6
# Portions Copyright (C) 2005 Canonical Ltd. (author: Scott James Remnant <scott@ubuntu.com>)
7
# Portions Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de>
9
# This program is free software; you can redistribute it and/or
10
# modify it under the terms of the GNU General Public License
11
# as published by the Free Software Foundation; either version 2
12
# of the License, or (at your option) any later version.
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# GNU General Public License for more details.
19
# You should have received a copy of the GNU General Public License
20
# along with this program; if not, write to the Free Software
21
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
26
from PyQt4 import QtCore, QtGui
28
from bzrlib.errors import NoSuchRevision, PathsNotVersionedError
29
from bzrlib.mutabletree import MutableTree
30
from bzrlib.patiencediff import PatienceSequenceMatcher as SequenceMatcher
31
from bzrlib.revisiontree import RevisionTree
32
from bzrlib.transform import _PreviewTree
33
from bzrlib.workingtree import WorkingTree
34
from bzrlib.workingtree_4 import DirStateRevisionTree
35
from bzrlib import trace
37
from bzrlib.plugins.qbzr.lib.diffview import (
41
from bzrlib.plugins.qbzr.lib.diff import (
47
from bzrlib.plugins.qbzr.lib.i18n import gettext, ngettext, N_
48
from bzrlib.plugins.qbzr.lib.util import (
49
BTN_CLOSE, BTN_REFRESH,
59
from bzrlib.plugins.qbzr.lib.uifactory import ui_current_widget
60
from bzrlib.plugins.qbzr.lib.trace import reports_exception
63
def get_file_lines_from_tree(tree, file_id):
65
return tree.get_file_lines(file_id)
66
except AttributeError:
67
return tree.get_file(file_id).readlines()
69
def get_title_for_tree(tree, branch, other_branch):
71
if None not in (branch, other_branch) and branch.base != other_branch.base:
72
branch_title = branch.nick
74
if isinstance(tree, WorkingTree):
76
return gettext("Working Tree for %s") % branch_title
78
return gettext("Working Tree")
80
elif isinstance(tree, (RevisionTree, DirStateRevisionTree)):
81
# revision_id_to_revno is faster, but only works on mainline rev
82
revid = tree.get_revision_id()
84
revno = branch.revision_id_to_revno(revid)
85
except NoSuchRevision:
87
revno_map = branch.get_revision_id_to_revno_map()
88
revno_tuple = revno_map[revid] # this can raise KeyError is revision not in the branch
89
revno = ".".join("%d" % i for i in revno_tuple)
91
# this can happens when you try to diff against other branch
97
return gettext("Rev %(rev)s for %(branch)s") % {"rev": revno, "branch": branch_title}
99
return gettext("Rev %s") % revno
102
return gettext("Revid: %(revid)s for %(branch)s") % {"revid": revid, "branch": branch_title}
104
return gettext("Revid: %s") % revid
106
elif isinstance(tree, _PreviewTree):
107
return gettext('Merge Preview')
109
# XXX I don't know what other cases we need to handle
110
return 'Unknown tree'
113
class DiffWindow(QBzrWindow):
115
def __init__(self, arg_provider, parent=None,
116
complete=False, encoding=None,
117
filter_options=None, ui_mode=True):
119
title = [gettext("Diff"), gettext("Loading...")]
120
QBzrWindow.__init__(self, title, parent, ui_mode=ui_mode)
121
self.restoreSize("diff", (780, 580))
124
self.encoding = encoding
125
self.arg_provider = arg_provider
126
self.filter_options = filter_options
127
if filter_options is None:
128
self.filter_options = FilterOptions(all_enable=True)
129
self.complete = complete
131
self.throbber = ThrobberWidget(self)
133
self.diffview = SidebySideDiffView(self)
134
self.sdiffview = SimpleDiffView(self)
135
self.views = (self.diffview, self.sdiffview)
137
self.stack = QtGui.QStackedWidget(self.centralwidget)
138
self.stack.addWidget(self.diffview)
139
self.stack.addWidget(self.sdiffview)
141
vbox = QtGui.QVBoxLayout(self.centralwidget)
142
vbox.addWidget(self.throbber)
143
vbox.addWidget(self.stack)
145
diffsidebyside = QtGui.QRadioButton(gettext("Side by side"),
147
self.connect(diffsidebyside,
148
QtCore.SIGNAL("clicked(bool)"),
149
self.click_diffsidebyside)
150
diffsidebyside.setChecked(True);
152
unidiff = QtGui.QRadioButton(gettext("Unidiff"), self.centralwidget)
153
self.connect(unidiff,
154
QtCore.SIGNAL("clicked(bool)"),
157
complete = QtGui.QCheckBox (gettext("Complete"),
159
self.connect(complete,
160
QtCore.SIGNAL("clicked(bool)"),
162
complete.setChecked(self.complete);
165
self.menu = ExtDiffMenu(include_builtin = False)
166
ext_diff_button = QtGui.QPushButton(gettext('Using'), self)
167
ext_diff_button.setMenu(self.menu)
168
self.connect(self.menu, QtCore.SIGNAL("triggered(QString)"),
169
self.ext_diff_triggered)
171
buttonbox = self.create_button_box(BTN_CLOSE)
173
refresh = StandardButton(BTN_REFRESH)
174
refresh.setEnabled(self.can_refresh())
175
buttonbox.addButton(refresh, QtGui.QDialogButtonBox.ActionRole)
176
self.connect(refresh,
177
QtCore.SIGNAL("clicked()"),
179
self.refresh_button = refresh
181
hbox = QtGui.QHBoxLayout()
182
hbox.addWidget(diffsidebyside)
183
hbox.addWidget(unidiff)
184
hbox.addWidget(complete)
186
hbox.addWidget(ext_diff_button)
187
hbox.addWidget(buttonbox)
191
QBzrWindow.show(self)
192
QtCore.QTimer.singleShot(1, self.initial_load)
194
@runs_in_loading_queue
197
def initial_load(self):
198
"""Called to perform the initial load of the form. Enables a
199
throbber window, then loads the branches etc if they weren't specified
202
# we only open the branch using the throbber
205
self.load_branch_info()
210
def load_branch_info(self):
214
specific_files) = self.arg_provider.get_diff_window_args(self.processEvents)
216
self.trees = (tree1, tree2)
217
self.branches = (branch1, branch2)
218
self.specific_files = specific_files
220
self.set_diff_title()
222
self.encodings = (get_set_encoding(self.encoding, branch1),
223
get_set_encoding(self.encoding, branch2))
226
def set_diff_title(self):
227
rev1_title = get_title_for_tree(self.trees[0], self.branches[0],
229
rev2_title = get_title_for_tree(self.trees[1], self.branches[1],
232
title = [gettext("Diff"), "%s..%s" % (rev1_title, rev2_title)]
234
if self.specific_files:
235
nfiles = len(self.specific_files)
238
ngettext("%d file", "%d files", nfiles) % nfiles)
240
title.append(", ".join(self.specific_files))
242
if self.filter_options and not self.filter_options.is_all_enable():
243
title.append(self.filter_options.to_str())
245
self.set_title_and_icon(title)
249
self.refresh_button.setEnabled(False)
250
for tree in self.trees: tree.lock_read()
253
changes = self.trees[1].iter_changes(self.trees[0],
254
specific_files=self.specific_files,
255
require_versioned=True)
256
def changes_key(change):
257
old_path, new_path = change[1]
264
no_changes = True # if there is no changes found we need to inform the user
265
for (file_id, paths, changed_content, versioned, parent, name, kind,
266
executable) in sorted(changes, key=changes_key):
267
# file_id -> ascii string
268
# paths -> 2-tuple (old, new) fullpaths unicode/None
269
# changed_content -> bool
270
# versioned -> 2-tuple (bool, bool)
272
# name -> 2-tuple (old_name, new_name) utf-8?/None
273
# kind -> 2-tuple (string/None, string/None)
274
# executable -> 2-tuple (bool/None, bool/None)
275
# NOTE: None value used for non-existing entry in corresponding
276
# tree, e.g. for added/deleted file
280
if parent == (None, None): # filter out TREE_ROOT (?)
283
# check for manually deleted files (w/o using bzr rm commands)
285
if versioned == (False, True):
288
if versioned == (True, True):
289
versioned = (True, False)
290
paths = (paths[0], None)
292
renamed = (parent[0], name[0]) != (parent[1], name[1])
298
dates[ix] = self.trees[ix].get_file_mtime(file_id, paths[ix])
300
if not renamed or e.errno != errno.ENOENT:
302
# If we get ENOENT error then probably we trigger
303
# bug #251532 in bzrlib. Take current time instead
304
dates[ix] = time.time()
306
properties_changed = []
307
if bool(executable[0]) != bool(executable[1]):
308
descr = {True: "+x", False: "-x", None: None}
309
properties_changed.append((descr[executable[0]],
310
descr[executable[1]]))
312
if versioned == (True, False):
313
status = N_('removed')
314
elif versioned == (False, True):
316
elif renamed and changed_content:
317
status = N_('renamed and modified')
319
status = N_('renamed')
321
status = N_('modified')
322
# check filter options
323
if not self.filter_options.check(status):
326
if ((versioned[0] != versioned[1] or changed_content)
327
and (kind[0] == 'file' or kind[1] == 'file')):
330
for ix, tree in enumerate(self.trees):
332
if versioned[ix] and kind[ix] == 'file':
333
content = get_file_lines_from_tree(tree, file_id)
334
lines.append(content)
335
binary = binary or is_binary_content(content)
338
if versioned == (True, False):
339
groups = [[('delete', 0, len(lines[0]), 0, 0)]]
340
elif versioned == (False, True):
341
groups = [[('insert', 0, 0, 0, len(lines[1]))]]
343
matcher = SequenceMatcher(None, lines[0], lines[1])
346
groups = list([matcher.get_opcodes()])
348
groups = list(matcher.get_grouped_opcodes())
350
lines = [[i.decode(encoding) for i in l]
351
for l, encoding in zip(lines, self.encodings)]
352
except UnicodeDecodeError, e:
353
trace.note('Failed to decode using %s, falling back to latin1' % e.encoding)
354
lines = [[i.decode('latin1') for i in l] for l in lines]
358
data = [''.join(l) for l in lines]
364
for view in self.views:
365
view.append_diff(list(paths), file_id, kind, status,
366
dates, versioned, binary, lines, groups,
367
data, properties_changed)
370
except PathsNotVersionedError, e:
371
QtGui.QMessageBox.critical(self, gettext('Diff'),
372
gettext(u'File %s is not versioned.\n'
373
'Operation aborted.') % e.paths_as_string,
377
for tree in self.trees: tree.unlock()
379
QtGui.QMessageBox.information(self, gettext('Diff'),
380
gettext('No changes found.'),
382
self.refresh_button.setEnabled(self.can_refresh())
384
def click_unidiff(self, checked):
386
self.sdiffview.rewind()
387
self.stack.setCurrentIndex(1)
389
def click_diffsidebyside(self, checked):
391
self.diffview.rewind()
392
self.stack.setCurrentIndex(0)
394
def click_complete(self, checked ):
395
self.complete = checked
396
#Has the side effect of refreshing...
397
self.diffview.clear()
398
self.sdiffview.clear()
399
run_in_loading_queue(self.load_diff)
401
def click_refresh(self):
402
self.diffview.clear()
403
self.sdiffview.clear()
404
run_in_loading_queue(self.load_diff)
406
def can_refresh(self):
407
"""Does any of tree is Mutanble/Working tree."""
408
if self.trees is None: # we might still be loading...
410
tree1, tree2 = self.trees
411
if isinstance(tree1, MutableTree) or isinstance(tree2, MutableTree):
415
def ext_diff_triggered(self, ext_diff):
416
show_diff(self.arg_provider, ext_diff=ext_diff, parent_window = self)