Merge lp:~hid-iwata/qbzr/extdiff-improve into lp:qbzr

Proposed by IWATA Hidetaka
Status: Merged
Merged at revision: 1467
Proposed branch: lp:~hid-iwata/qbzr/extdiff-improve
Merge into: lp:qbzr
Diff against target: 954 lines (+659/-84)
12 files modified
NEWS.txt (+6/-0)
lib/commit.py (+4/-2)
lib/diff.py (+311/-5)
lib/diffwindow.py (+32/-57)
lib/logwidget.py (+2/-2)
lib/revert.py (+2/-1)
lib/revtreeview.py (+3/-1)
lib/tests/__init__.py (+1/-0)
lib/tests/mock.py (+7/-2)
lib/tests/test_extdiff.py (+288/-0)
lib/treewidget.py (+2/-2)
lib/widgets/shelvelist.py (+1/-12)
To merge this branch: bzr merge lp:~hid-iwata/qbzr/extdiff-improve
Reviewer Review Type Date Requested Status
Alexander Belchenko Approve
Review via email: mp+94326@code.launchpad.net

Description of the change

Change behaivior of external diff.

1. Allow to open multiple diff at once.
   Currently, if we launch extenal diff for two or more files, diff tool is launched one by one.
   Second file diff is shown only after first diff is closed.
   By this patch, all diffs are shown at once. And all diffs are shown in the single window if diff tool supports it.

2. Reduce lock
   Currently, external diff keeps trees locked until diff tool is killed.
   By this patch, trees are locked only when starting up of diff tool.

3. Performance improvement
   Performance improvement by removing needless bzr call.

NOTE:
I don't update NEWS yet because I don't know this patch should be merged to either lp:qbzr or lp:qbzr/0.22
Please tell me if this patch is acceptable, and I'll update NEWS and merge this by myself.

To post a comment you must log in.
lp:~hid-iwata/qbzr/extdiff-improve updated
1468. By IWATA Hidetaka

fix wrong comment.

Revision history for this message
Alexander Belchenko (bialix) wrote :

IWATA Hidetaka пишет:
> I don't update NEWS yet because I don't know this patch should be merged to either lp:qbzr or lp:qbzr/0.22

I'd prefer to have it merged into lp:qbzr. Because this patch introduces
a lot of changes, and I'd like to avoid landing it to stable branch, so
we can have more time for polishing it before bzr 2.6 / qbzr 0.23 final
releases.

--
All the dude wanted was his rug back

lp:~hid-iwata/qbzr/extdiff-improve updated
1469. By IWATA Hidetaka

merge from trunk (start 0.23)

1470. By IWATA Hidetaka

update NEWS

Revision history for this message
IWATA Hidetaka (hid-iwata) wrote :

OK. I've update NEWS.txt for 0.23.

Revision history for this message
Alexander Belchenko (bialix) wrote :

Hidetaka, I'm still reading your patch, but I have one note for you.
There is one thing I try to avoid whenever it's possible: I try to don't break public methods signature if I can.

You changed signature of show_diff to

def show_diff(arg_provider, context=None, ext_diff=None, parent_window=None):

Why don't you put your new optional context to the end of parameters list? What makes it so special?

Maybe it's worth to create new method with such signature, e.g. show_diff_in_context(...) and then change show_diff() to call show_diff_in_context(...) with context=None?

Maybe it doesn't matter, but maybe some other code out there depends on the current show_diff implementation. If we can keep backward compatibility, I'd like to keep it.

lp:~hid-iwata/qbzr/extdiff-improve updated
1471. By IWATA Hidetaka

Change argument order of show_diff (To avoid compatibility break)

Revision history for this message
IWATA Hidetaka (hid-iwata) wrote :

Thank you for feedback.
Your indication is quite right. This is my mistake.
I've pushed fix for this now.

Revision history for this message
Alexander Belchenko (bialix) wrote :

Please, land it.

We may want to announce this new functionality in qbzr/bzr ML and ask early adopters to test it.

review: Approve
Revision history for this message
IWATA Hidetaka (hid-iwata) wrote :

I've landed now.
Thank you.

2012年2月27日22:27 Alexander Belchenko <email address hidden>:
> Review: Approve
>
> Please, land it.
>
> We may want to announce this new functionality in qbzr/bzr ML and ask early adopters to test it.
> --
> https://code.launchpad.net/~hid-iwata/qbzr/extdiff-improve/+merge/94326
> You are the owner of lp:~hid-iwata/qbzr/extdiff-improve.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS.txt'
2--- NEWS.txt 2012-02-22 06:30:27 +0000
3+++ NEWS.txt 2012-02-24 14:57:59 +0000
4@@ -1,6 +1,12 @@
5 0.23.0dev - in development
6 --------------------------
7+What's new in this release:
8
9+Behavior change: External diff behavior is changed as below.
10+ 1. When calling external diff for multiple files, all diffs shown at once.
11+ 2. Working tree or branch is locked only when starting up of diff tools.
12+This affects to all commands which use external diff.
13+(qdiff, qcommit, qbrowse, qannotate, ...)
14
15 0.22.2 - in development
16 -----------------------
17
18=== modified file 'lib/commit.py'
19--- lib/commit.py 2012-02-21 13:03:18 +0000
20+++ lib/commit.py 2012-02-24 14:57:59 +0000
21@@ -779,7 +779,8 @@
22 self.tree.branch, self.tree.branch,
23 specific_files=checked)
24
25- show_diff(arg_provider, ext_diff=ext_diff, parent_window = self)
26+ show_diff(arg_provider, ext_diff=ext_diff, parent_window=self,
27+ context=self.filelist.diff_context)
28 else:
29 msg = "No changes selected to " + dialog_action
30 QtGui.QMessageBox.warning(self,
31@@ -794,7 +795,8 @@
32 arg_provider = InternalWTDiffArgProvider(
33 self.tree.basis_tree().get_revision_id(), self.tree,
34 self.tree.branch, self.tree.branch)
35- show_diff(arg_provider, ext_diff=ext_diff, parent_window = self)
36+ show_diff(arg_provider, ext_diff=ext_diff, parent_window=self,
37+ context=self.filelist.diff_context)
38
39 def on_failed(self, error):
40 SubProcessDialog.on_failed(self, error)
41
42=== modified file 'lib/diff.py'
43--- lib/diff.py 2011-12-28 11:34:27 +0000
44+++ lib/diff.py 2012-02-24 14:57:59 +0000
45@@ -19,7 +19,7 @@
46
47 from PyQt4 import QtCore, QtGui
48
49-from bzrlib.errors import FileTimestampUnavailable
50+from bzrlib.errors import FileTimestampUnavailable, NoSuchId, ExecutableMissing
51 from bzrlib.plugins.qbzr.lib.diff_arg import * # import DiffArgProvider classes
52 from bzrlib.plugins.qbzr.lib.i18n import gettext
53 from bzrlib.plugins.qbzr.lib.subprocess import SimpleSubProcessDialog
54@@ -33,10 +33,17 @@
55 import errno
56 import re
57 import time
58+import sys
59+import os
60+import glob
61 from bzrlib.patiencediff import PatienceSequenceMatcher as SequenceMatcher
62 from bzrlib.plugins.qbzr.lib.i18n import gettext, ngettext, N_
63-from bzrlib import trace
64+from bzrlib import trace, osutils, cmdline
65+from bzrlib.workingtree import WorkingTree
66+from bzrlib.trace import mutter
67 ''')
68+from bzrlib.diff import DiffFromTool, DiffPath
69+subprocess = __import__('subprocess', {}, {}, [])
70
71 qconfig = get_qbzr_config()
72 default_diff = qconfig.get_option("default_diff")
73@@ -47,7 +54,7 @@
74 ext_diffs[name] = command
75
76
77-def show_diff(arg_provider, ext_diff=None, parent_window=None):
78+def show_diff(arg_provider, ext_diff=None, parent_window=None, context=None):
79
80 if ext_diff is None:
81 ext_diff = default_diff
82@@ -62,6 +69,25 @@
83 window.show()
84 if parent_window:
85 parent_window.windows.append(window)
86+ elif context:
87+ ext_diff = str(ext_diff) # convert QString to str
88+ cleanup = []
89+ try:
90+ args = arg_provider.get_diff_window_args(
91+ QtGui.QApplication.processEvents, cleanup.append
92+ )
93+ old_tree = args["old_tree"]
94+ new_tree = args["new_tree"]
95+ specific_files = args.get("specific_files")
96+ context.setup(ext_diff, old_tree, new_tree)
97+ if specific_files:
98+ context.diff_paths(specific_files)
99+ else:
100+ context.diff_tree()
101+ finally:
102+ while cleanup:
103+ cleanup.pop()()
104+
105 else:
106 args=["diff", "--using", ext_diff] # NEVER USE --using=xxx, ALWAYS --using xxx
107 # This should be move to after the window has been shown.
108@@ -157,7 +183,44 @@
109 """
110
111 @classmethod
112- def create(klass, trees, file_id, paths, changed_content, versioned,
113+ def iter_items(cls, trees, specific_files=None, filter=None, lock_trees=False):
114+ try:
115+ cleanup = []
116+ if lock_trees:
117+ for t in trees:
118+ cleanup.append(t.lock_read().unlock)
119+
120+ changes = trees[1].iter_changes(trees[0],
121+ specific_files=specific_files,
122+ require_versioned=True)
123+
124+ def changes_key(change):
125+ return change[1][1] or change[1][0]
126+
127+ for (file_id, paths, changed_content, versioned, parent, name, kind,
128+ executable) in sorted(changes, key=changes_key):
129+ # file_id -> ascii string
130+ # paths -> 2-tuple (old, new) fullpaths unicode/None
131+ # changed_content -> bool
132+ # versioned -> 2-tuple (bool, bool)
133+ # parent -> 2-tuple
134+ # name -> 2-tuple (old_name, new_name) utf-8?/None
135+ # kind -> 2-tuple (string/None, string/None)
136+ # executable -> 2-tuple (bool/None, bool/None)
137+ # NOTE: None value used for non-existing entry in corresponding
138+ # tree, e.g. for added/deleted file
139+ di = DiffItem.create(trees, file_id, paths, changed_content,
140+ versioned, parent, name, kind, executable,
141+ filter = filter)
142+ if not di:
143+ continue
144+ yield di
145+ finally:
146+ while cleanup:
147+ cleanup.pop()()
148+
149+ @classmethod
150+ def create(cls, trees, file_id, paths, changed_content, versioned,
151 parent, name, kind, executable, filter = None):
152
153 if parent == (None, None): # filter out TREE_ROOT (?)
154@@ -209,7 +272,7 @@
155 if filter and not filter(status):
156 return None
157
158- return klass(trees, file_id, paths, changed_content, versioned, kind,
159+ return cls(trees, file_id, paths, changed_content, versioned, kind,
160 properties_changed, dates, status)
161
162 def __init__(self, trees, file_id, paths, changed_content, versioned, kind,
163@@ -324,3 +387,246 @@
164 ulines[i] = [l.decode(encodings[i], 'replace') for l in lines[i]]
165 return ulines
166
167+CACHE_TIMEOUT = 3600
168+
169+class _ExtDiffer(DiffFromTool):
170+ """
171+ Run extdiff async.
172+ XXX: This class is strongly depending on DiffFromTool internals now.
173+ """
174+ def __init__(self, command_string, old_tree, new_tree, to_file=None, path_encoding='utf-8'):
175+ DiffPath.__init__(self, old_tree, new_tree, to_file or sys.stdout, path_encoding)
176+ self.set_command_string(command_string)
177+ parent = osutils.joinpath([osutils.tempfile.gettempdir(), 'qbzr'])
178+ if not os.path.isdir(parent):
179+ os.mkdir(parent)
180+ self._root = osutils.mkdtemp(prefix='qbzr/bzr-diff-')
181+ self.prefixes = {}
182+ self._set_prefix()
183+
184+ @property
185+ def trees(self):
186+ return self.old_tree, self.new_tree
187+
188+ def set_trees(self, old_tree, new_tree):
189+ self.old_tree = old_tree
190+ self.new_tree = new_tree
191+ self._set_prefix()
192+
193+ def _set_prefix(self):
194+ self.old_prefix = self.get_prefix(self.old_tree)
195+ self.new_prefix = self.get_prefix(self.new_tree)
196+
197+ def get_prefix(self, tree):
198+ def get_key(tree):
199+ if hasattr(tree, "get_revision_id"):
200+ return tree.__class__.__name__ + ":" + tree.get_revision_id()
201+ elif hasattr(tree, "abspath"):
202+ return tree.__class__.__name__ + ":" + tree.abspath("")
203+ else:
204+ return tree.__class__.__name__ + ":" + str(hash(tree))
205+
206+ if tree is None:
207+ return None
208+ key = get_key(tree)
209+ if self.prefixes.has_key(key):
210+ return self.prefixes[key]
211+ prefix = str(len(self.prefixes) + 1)
212+ self.prefixes[key] = prefix
213+ return prefix
214+
215+ def set_command_string(self, command_string):
216+ command_template = cmdline.split(command_string)
217+ if '@' not in command_string:
218+ command_template.extend(['@old_path', '@new_path'])
219+ self.command_template = command_template
220+
221+ def finish(self):
222+ parent = os.path.dirname(self._root)
223+ for path in glob.glob(os.path.join(parent, "*")):
224+ if self._is_deletable(path):
225+ self._delete_tmpdir(path)
226+ self._delete_tmpdir(self._root)
227+
228+ def finish_lazy(self):
229+ parent = os.path.dirname(self._root)
230+ for path in glob.glob(os.path.join(parent, "*")):
231+ if self._is_deletable(path):
232+ self._delete_tmpdir(path)
233+ open(os.path.join(self._root, ".delete"), "w").close()
234+
235+ def _is_deletable(self, root):
236+ if os.path.exists(os.path.join(root, ".delete")):
237+ return True
238+ elif time.time() > os.path.getctime(root) + CACHE_TIMEOUT:
239+ return True
240+ else:
241+ return False
242+
243+ def _delete_tmpdir(self, path):
244+ try:
245+ osutils.rmtree(path)
246+ except:
247+ if os.path.isdir(path):
248+ open(os.path.join(path, ".delete"), "w").close()
249+
250+ def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
251+ allow_write=False):
252+ if force_temp or not isinstance(tree, WorkingTree):
253+ full_path = self._safe_filename(prefix, relpath)
254+ if os.path.isfile(full_path):
255+ return full_path
256+ return DiffFromTool._write_file(self, file_id, tree, prefix,
257+ relpath, force_temp, allow_write)
258+
259+ def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
260+ allow_write_new=False):
261+ old_disk_path = self._write_file(file_id, self.old_tree,
262+ self.old_prefix, old_path, force_temp)
263+ new_disk_path = self._write_file(file_id, self.new_tree,
264+ self.new_prefix, new_path, force_temp,
265+ allow_write=allow_write_new)
266+ return old_disk_path, new_disk_path
267+
268+ def _execute(self, old_path, new_path):
269+ command = self._get_command(old_path, new_path)
270+ try:
271+ subprocess.Popen(command, cwd=self._root)
272+ except OSError, e:
273+ if e.errno == errno.ENOENT:
274+ raise ExecutableMissing(command[0])
275+ else:
276+ raise
277+ return 0
278+
279+ def diff(self, file_id):
280+ try:
281+ new_path = self.new_tree.id2path(file_id)
282+ new_kind = self.new_tree.kind(file_id)
283+ old_path = self.old_tree.id2path(file_id)
284+ old_kind = self.old_tree.kind(file_id)
285+ except NoSuchId:
286+ return DiffPath.CANNOT_DIFF
287+ return DiffFromTool.diff(self, file_id, old_path, new_path, old_kind, new_kind)
288+
289+class ExtDiffContext(QtCore.QObject):
290+ """
291+ Environment for external diff execution.
292+ This class manages cache of diffed files and when it will be deleted.
293+ """
294+ def __init__(self, parent, to_file=None, path_encoding='utf-8'):
295+ """
296+ :parent: parent widget. If specified, cache is deleted automatically
297+ when parent is closed.
298+ :to_file: stream to write output messages. If not specified,
299+ stdout is used.
300+ :path_encoding: encoding of path
301+ """
302+ QtCore.QObject.__init__(self, parent)
303+ self.to_file = to_file
304+ self.path_encoding = path_encoding
305+ self._differ = None
306+ if parent is not None:
307+ parent.window().installEventFilter(self)
308+
309+ def finish(self):
310+ """
311+ Remove temporary directory which contains cached files.
312+ """
313+ try:
314+ if self._differ:
315+ self._differ.finish()
316+ self._differ = None
317+ except:
318+ pass
319+
320+ def finish_lazy(self):
321+ """
322+ Mark temporary directory as deletable, without delete it actually.
323+ XXX: Directory marked as deletable will be deleted next time.
324+ """
325+ try:
326+ if self._differ:
327+ self._differ.finish_lazy()
328+ self._differ = None
329+ except:
330+ pass
331+
332+ @property
333+ def rootdir(self):
334+ if self._differ:
335+ return self._differ._root
336+ else:
337+ return None
338+
339+ def setup(self, command_string, old_tree, new_tree):
340+ """
341+ Set or change diff command and diffed trees.
342+ """
343+ if self._differ is None:
344+ self._differ = _ExtDiffer(command_string, old_tree, new_tree,
345+ self.to_file, self.path_encoding)
346+ else:
347+ self._differ.set_trees(old_tree, new_tree)
348+ self._differ.set_command_string(command_string)
349+
350+ def eventFilter(self, obj, event):
351+ if event.type() == QtCore.QEvent.Close:
352+ self.finish()
353+ return QtCore.QObject.eventFilter(self, obj, event)
354+
355+ def diff_ids(self, file_ids, interval=50, lock_trees=True):
356+ """
357+ Show diffs of specified file_ids.
358+ NOTE: Directories cannot be specified.
359+ Use diff_tree or diff_paths instead when specifing directory.
360+ """
361+ cleanup = []
362+ try:
363+ if lock_trees:
364+ cleanup.append(self._differ.new_tree.lock_read().unlock)
365+ cleanup.append(self._differ.old_tree.lock_read().unlock)
366+ for file_id in file_ids:
367+ self._differ.diff(file_id)
368+ time.sleep(interval * 0.001)
369+ finally:
370+ while cleanup:
371+ cleanup.pop()()
372+
373+ def diff_paths(self, paths, interval=50, lock_trees=True):
374+ """
375+ Show diffs of specified file paths.
376+ """
377+ new_tree = self._differ.new_tree
378+
379+ valid_paths = []
380+ ids = []
381+ dir_included = False
382+ for p in paths:
383+ id = new_tree.path2id(p)
384+ if id:
385+ valid_paths.append(p)
386+ ids.append(id)
387+ dir_included = dir_included or (new_tree.kind(id) != 'file')
388+ else:
389+ mutter('%s does not exist in the new tree' % p)
390+ if not ids:
391+ return
392+ if dir_included:
393+ self.diff_tree(valid_paths, interval, lock_trees)
394+ else:
395+ self.diff_ids(ids, interval, lock_trees)
396+
397+ def diff_tree(self, specific_files=None, interval=50, lock_trees=True):
398+ """
399+ Show diffs between two trees. (trees must be set by setup method)
400+ NOTE: Directory path can be specified to specific_files.
401+ """
402+ for di in DiffItem.iter_items(self._differ.trees,
403+ specific_files=specific_files,
404+ lock_trees=lock_trees):
405+ if di.changed_content:
406+ self._differ.diff(di.file_id)
407+ time.sleep(interval * 0.001)
408+
409+
410
411=== modified file 'lib/diffwindow.py'
412--- lib/diffwindow.py 2012-01-30 16:55:25 +0000
413+++ lib/diffwindow.py 2012-02-24 14:57:59 +0000
414@@ -45,6 +45,7 @@
415 has_ext_diff,
416 ExtDiffMenu,
417 DiffItem,
418+ ExtDiffContext,
419 )
420
421 from bzrlib.plugins.qbzr.lib.i18n import gettext, ngettext, N_
422@@ -162,6 +163,7 @@
423 setup_guidebar_for_find(self.sdiffview, self.find_toolbar, 1)
424 for gb in self.diffview.guidebar_panels:
425 setup_guidebar_for_find(gb, self.find_toolbar, 1)
426+ self.diff_context = ExtDiffContext(self)
427
428 def connect_later(self, *args, **kwargs):
429 """Schedules a signal to be connected after loading CLI arguments.
430@@ -422,64 +424,36 @@
431
432 def load_diff(self):
433 self.view_refresh.setEnabled(False)
434- for tree in self.trees: tree.lock_read()
435 self.processEvents()
436+
437 try:
438- changes = self.trees[1].iter_changes(self.trees[0],
439- specific_files=self.specific_files,
440- require_versioned=True)
441- def changes_key(change):
442- old_path, new_path = change[1]
443- path = new_path
444- if path is None:
445- path = old_path
446- return path
447-
448- try:
449- no_changes = True # if there is no changes found we need to inform the user
450- for (file_id, paths, changed_content, versioned, parent, name, kind,
451- executable) in sorted(changes, key=changes_key):
452- # file_id -> ascii string
453- # paths -> 2-tuple (old, new) fullpaths unicode/None
454- # changed_content -> bool
455- # versioned -> 2-tuple (bool, bool)
456- # parent -> 2-tuple
457- # name -> 2-tuple (old_name, new_name) utf-8?/None
458- # kind -> 2-tuple (string/None, string/None)
459- # executable -> 2-tuple (bool/None, bool/None)
460- # NOTE: None value used for non-existing entry in corresponding
461- # tree, e.g. for added/deleted file
462-
463- self.processEvents()
464- di = DiffItem.create(self.trees, file_id, paths, changed_content,
465- versioned, parent, name, kind, executable,
466- filter = self.filter_options.check)
467- if not di:
468- continue
469-
470- lines = di.lines
471- self.processEvents()
472- groups = di.groups(self.complete, self.ignore_whitespace)
473- self.processEvents()
474- ulines = di.get_unicode_lines(
475- (self.encoding_selector_left.encoding,
476- self.encoding_selector_right.encoding))
477- data = [''.join(l) for l in ulines]
478-
479- for view in self.views:
480- view.append_diff(list(di.paths), di.file_id, di.kind, di.status,
481- di.dates, di.versioned, di.binary, ulines, groups,
482- data, di.properties_changed)
483- self.processEvents()
484- no_changes = False
485- except PathsNotVersionedError, e:
486- QtGui.QMessageBox.critical(self, gettext('Diff'),
487- gettext(u'File %s is not versioned.\n'
488- 'Operation aborted.') % e.paths_as_string,
489- gettext('&Close'))
490- self.close()
491- finally:
492- for tree in self.trees: tree.unlock()
493+ no_changes = True # if there is no changes found we need to inform the user
494+ for di in DiffItem.iter_items(self.trees,
495+ specific_files=self.specific_files,
496+ filter=self.filter_options.check,
497+ lock_trees=True):
498+ lines = di.lines
499+ self.processEvents()
500+ groups = di.groups(self.complete, self.ignore_whitespace)
501+ self.processEvents()
502+ ulines = di.get_unicode_lines(
503+ (self.encoding_selector_left.encoding,
504+ self.encoding_selector_right.encoding))
505+ data = [''.join(l) for l in ulines]
506+
507+ for view in self.views:
508+ view.append_diff(list(di.paths), di.file_id, di.kind, di.status,
509+ di.dates, di.versioned, di.binary, ulines, groups,
510+ data, di.properties_changed)
511+ self.processEvents()
512+ no_changes = False
513+ except PathsNotVersionedError, e:
514+ QtGui.QMessageBox.critical(self, gettext('Diff'),
515+ gettext(u'File %s is not versioned.\n'
516+ 'Operation aborted.') % e.paths_as_string,
517+ gettext('&Close'))
518+ self.close()
519+
520 if no_changes:
521 QtGui.QMessageBox.information(self, gettext('Diff'),
522 gettext('No changes found.'),
523@@ -530,7 +504,8 @@
524
525 def ext_diff_triggered(self, ext_diff):
526 """@param ext_diff: path to external diff executable."""
527- show_diff(self.arg_provider, ext_diff=ext_diff, parent_window = self)
528+ show_diff(self.arg_provider, ext_diff=ext_diff, parent_window=self,
529+ context=self.diff_context)
530
531 def click_ignore_whitespace(self, checked ):
532 self.ignore_whitespace = checked
533
534=== modified file 'lib/logwidget.py'
535--- lib/logwidget.py 2011-08-05 13:01:37 +0000
536+++ lib/logwidget.py 2012-02-24 14:57:59 +0000
537@@ -551,8 +551,8 @@
538 specific_file_ids = specific_file_ids)
539
540
541- diff.show_diff(arg_provider, ext_diff = ext_diff,
542- parent_window = self.window())
543+ diff.show_diff(arg_provider, ext_diff=ext_diff,
544+ parent_window=self.window(), context=self.diff_context)
545
546 def show_diff_specified_files(self, ext_diff=None):
547 if self.log_model.file_id_filter:
548
549=== modified file 'lib/revert.py'
550--- lib/revert.py 2010-09-14 18:01:29 +0000
551+++ lib/revert.py 2012-02-24 14:57:59 +0000
552@@ -283,7 +283,8 @@
553 self.tree.branch, self.tree.branch,
554 specific_files=checked)
555
556- show_diff(arg_provider, ext_diff=ext_diff, parent_window=self)
557+ show_diff(arg_provider, ext_diff=ext_diff, parent_window=self,
558+ context=self.filelist.diff_context)
559
560 else:
561 msg = "No changes selected to " + dialog_action
562
563=== modified file 'lib/revtreeview.py'
564--- lib/revtreeview.py 2010-10-01 20:16:08 +0000
565+++ lib/revtreeview.py 2012-02-24 14:57:59 +0000
566@@ -23,6 +23,7 @@
567 lazy_import(globals(), '''
568 from bzrlib.plugins.qbzr.lib.util import run_in_loading_queue
569 from bzrlib.plugins.qbzr.lib.lazycachedrevloader import load_revisions
570+from bzrlib.plugins.qbzr.lib.diff import ExtDiffContext
571 from bzrlib.transport.local import LocalTransport
572 ''')
573
574@@ -57,7 +58,8 @@
575 self.load_revisions_call_count = 0
576 self.load_revisions_throbber_shown = False
577 self.revision_loading_disabled = False
578-
579+ self.diff_context = ExtDiffContext(self)
580+
581 def setModel(self, model):
582 QtGui.QTreeView.setModel(self, model)
583
584
585=== modified file 'lib/tests/__init__.py'
586--- lib/tests/__init__.py 2012-02-06 16:13:20 +0000
587+++ lib/tests/__init__.py 2012-02-24 14:57:59 +0000
588@@ -54,6 +54,7 @@
589 'test_util',
590 'test_decorator',
591 'test_guidebar',
592+ 'test_extdiff',
593 ]
594 for name in testmod_names:
595 m = "%s.%s" % (__name__, name)
596
597=== modified file 'lib/tests/mock.py'
598--- lib/tests/mock.py 2009-06-15 14:39:03 +0000
599+++ lib/tests/mock.py 2012-02-24 14:57:59 +0000
600@@ -26,14 +26,19 @@
601 and which arguments were used.
602 """
603
604- def __init__(self):
605+ def __init__(self, func=None, ret=None):
606 self.count = 0
607 self.args = []
608+ self._func = func
609+ self.ret = ret
610
611 def __call__(self, *args, **kw):
612 self.count += 1
613 self.args.append((args, kw))
614-
615+ if self._func is not None:
616+ return self._func(*args, **kw)
617+ else:
618+ return self.ret
619
620 class TestMockFunction(TestCase):
621
622
623=== added file 'lib/tests/test_extdiff.py'
624--- lib/tests/test_extdiff.py 1970-01-01 00:00:00 +0000
625+++ lib/tests/test_extdiff.py 2012-02-24 14:57:59 +0000
626@@ -0,0 +1,288 @@
627+if __name__=='__main__':
628+ import bzrlib
629+ bzrlib.initialize()
630+ import bzrlib.plugin
631+ bzrlib.plugin.set_plugins_path()
632+ bzrlib.plugin.load_plugins()
633+
634+import os, tempfile
635+from bzrlib.plugins.qbzr.lib.tests import QTestCase
636+from bzrlib.plugins.qbzr.lib.tests.mock import MockFunction
637+from bzrlib.plugins.qbzr.lib import diff
638+from bzrlib.workingtree import WorkingTree
639+from contextlib import contextmanager
640+
641+
642+class TestCommandString(QTestCase):
643+ def setUp(self):
644+ QTestCase.setUp(self)
645+ self.tree = self.make_branch_and_tree('tree')
646+ self.build_tree_contents([('tree/a', "content")])
647+ self.tree.add(["a"])
648+ self.tree.commit(message='1')
649+ self.differ = diff._ExtDiffer("test", self.tree.basis_tree(), self.tree)
650+ self.addCleanup(self.differ.finish)
651+
652+ def test_no_arguments(self):
653+ self.differ.set_command_string("test")
654+ self.assertEqual(self.differ.command_template,
655+ ["test", "@old_path", "@new_path"])
656+
657+ def test_has_arguments(self):
658+ self.differ.set_command_string("test --old @old_path --new @new_path")
659+ self.assertEqual(self.differ.command_template,
660+ ["test", "--old", "@old_path", "--new", "@new_path"])
661+
662+class TestPrefix(QTestCase):
663+ def setUp(self):
664+ QTestCase.setUp(self)
665+ self.tree = self.make_branch_and_tree('tree')
666+ self.build_tree_contents([('tree/a', "content")])
667+ self.tree.add(["a"])
668+ self.tree.commit(message='1')
669+ self.build_tree_contents([('tree/a', "new content")])
670+ self.tree.commit(message='2')
671+ self.differ = diff._ExtDiffer("test", self.tree.basis_tree(), self.tree)
672+ self.addCleanup(self.differ.finish)
673+
674+ def test_same_prefix_for_same_workingtree(self):
675+ prefix = self.differ.get_prefix(self.tree)
676+ tree2 = WorkingTree.open(self.tree.abspath(""))
677+ self.assertEqual(self.differ.get_prefix(tree2), prefix)
678+
679+ def test_another_prefix_for_another_workingtree(self):
680+ tree2 = self.make_branch_and_tree('tree2')
681+ prefix = self.differ.get_prefix(self.tree)
682+ self.assertNotEqual(self.differ.get_prefix(tree2), prefix)
683+
684+ def test_same_prefix_for_same_revisiontree(self):
685+ self.differ = diff._ExtDiffer("test", self.tree.basis_tree(), self.tree)
686+ tree1 = self.tree.basis_tree()
687+ tree2 = self.tree.basis_tree()
688+ self.assertNotEqual(tree1, tree2)
689+ prefix = self.differ.get_prefix(tree1)
690+ self.assertEqual(self.differ.get_prefix(tree2), prefix)
691+ self.assertNotEqual(self.differ.get_prefix(self.tree), prefix)
692+
693+ def test_another_prefix_for_another_revtree(self):
694+ b = self.tree.branch
695+ trees = [b.repository.revision_tree(b.get_rev_id(no)) for no in (1,2)]
696+ prefix = self.differ.get_prefix(trees[0])
697+ self.assertNotEqual(self.differ.get_prefix(trees[1]), prefix)
698+
699+ def test_same_prefix_for_same_object(self):
700+ obj = object()
701+ prefix = self.differ.get_prefix(obj)
702+ self.assertEqual(self.differ.get_prefix(obj), prefix)
703+
704+ def test_another_prefix_for_another_object(self):
705+ obj = object()
706+ prefix = self.differ.get_prefix(obj)
707+ self.assertNotEqual(self.differ.get_prefix(object()), prefix)
708+
709+class TestExtDiffBase(QTestCase):
710+ def setUp(self):
711+ QTestCase.setUp(self)
712+ self.popen_mock = MockFunction()
713+ popen = diff.subprocess.Popen
714+ diff.subprocess.Popen = self.popen_mock
715+ def restore():
716+ diff.subprocess.Popen = popen
717+ self.addCleanup(restore)
718+ self.tree = self.make_branch_and_tree('tree')
719+ self.ctx = self.create_context()
720+ self.addCleanup(self.ctx.finish)
721+
722+ def create_context(self, parent=None):
723+ ctx = diff.ExtDiffContext(parent)
724+ self.addCleanup(ctx.finish)
725+ return ctx
726+
727+ def assertFileContent(self, path, content):
728+ self.assertTrue(os.path.isfile(path))
729+ f = open(path)
730+ self.assertEqual("\n".join(f.readlines()), content)
731+ f.close()
732+
733+class TestCleanup(TestExtDiffBase):
734+ def setUp(self):
735+ TestExtDiffBase.setUp(self)
736+ self.build_tree_contents([('tree/a', "a")])
737+ self.tree.add(['a'])
738+ self.tree.commit(message='1')
739+ self.build_tree_contents([('tree/a', "aa")])
740+
741+ def test_remove_root(self):
742+ self.ctx.setup("diff.txt", self.tree.basis_tree(), self.tree)
743+ rootdir = self.ctx.rootdir
744+ self.assertTrue(len(rootdir) > 0)
745+ self.assertTrue(os.path.isdir(rootdir))
746+ self.ctx.finish()
747+ self.assertTrue(self.ctx.rootdir is None)
748+ self.assertFalse(os.path.exists(rootdir))
749+
750+ def test_dont_remove_root_of_other_context(self):
751+ self.ctx.setup("diff.txt", self.tree.basis_tree(), self.tree)
752+ ctx2 = self.create_context()
753+ ctx2.setup("diff.txt", self.tree.basis_tree(), self.tree)
754+ rootdir = self.ctx.rootdir
755+ self.assertNotEqual(rootdir, ctx2.rootdir)
756+ self.ctx.finish()
757+ self.assertTrue(self.ctx.rootdir is None)
758+ self.assertTrue(os.path.exists(ctx2.rootdir))
759+
760+ @contextmanager
761+ def invalidate_rmtree(self):
762+ rmtree = diff.osutils.rmtree
763+ def rmtree_mock(path):
764+ raise IOError("osutils.rmtree is now invalidated.")
765+ try:
766+ diff.osutils.rmtree = MockFunction(func=rmtree_mock)
767+ yield
768+ finally:
769+ diff.osutils.rmtree = rmtree
770+
771+ def test_mark_deletable_if_delete_failed(self):
772+ # If failed to delete tempdir, mark it deletable to delete later.
773+ self.ctx.setup("diff.txt", self.tree.basis_tree(), self.tree)
774+ rootdir = self.ctx.rootdir
775+ self.assertTrue(len(rootdir) > 0)
776+ self.assertTrue(os.path.isdir(rootdir))
777+ with self.invalidate_rmtree():
778+ self.ctx.finish()
779+ self.assertTrue(self.ctx.rootdir is None)
780+ self.assertTrue(os.path.isfile(os.path.join(rootdir, ".delete")))
781+
782+ def test_cleanup_deletable_roots(self):
783+ ctx2 = self.create_context()
784+ ctx3 = self.create_context()
785+ self.ctx.setup("diff.txt", self.tree.basis_tree(), self.tree)
786+ ctx2.setup("diff.txt", self.tree.basis_tree(), self.tree)
787+ ctx3.setup("diff.txt", self.tree.basis_tree(), self.tree)
788+ root = self.ctx.rootdir
789+ root2 = ctx2.rootdir
790+ root3 = ctx3.rootdir
791+ with self.invalidate_rmtree():
792+ self.ctx.finish()
793+ self.assertTrue(os.path.isfile(os.path.join(root, ".delete")))
794+ os.chdir(tempfile.gettempdir())
795+ ctx2.finish()
796+ self.assertFalse(os.path.exists(root))
797+ self.assertFalse(os.path.exists(root2))
798+ self.assertTrue(os.path.exists(root3))
799+
800+ def test_finish_lazy(self):
801+ self.ctx.setup("diff.txt", self.tree.basis_tree(), self.tree)
802+ rootdir = self.ctx.rootdir
803+ self.assertTrue(len(rootdir) > 0)
804+ self.assertTrue(os.path.isdir(rootdir))
805+ self.ctx.finish_lazy()
806+ self.assertTrue(self.ctx.rootdir is None)
807+ self.assertTrue(os.path.isfile(os.path.join(rootdir, ".delete")))
808+
809+
810+ def test_cleanup_deletable_roots_by_finish_lazy(self):
811+ ctx2 = self.create_context()
812+ ctx3 = self.create_context()
813+ self.ctx.setup("diff.txt", self.tree.basis_tree(), self.tree)
814+ ctx2.setup("diff.txt", self.tree.basis_tree(), self.tree)
815+ ctx3.setup("diff.txt", self.tree.basis_tree(), self.tree)
816+ root = self.ctx.rootdir
817+ root2 = ctx2.rootdir
818+ root3 = ctx3.rootdir
819+ self.ctx.finish_lazy()
820+ ctx2.finish()
821+ self.assertFalse(os.path.exists(root))
822+ self.assertFalse(os.path.exists(root2))
823+ self.assertTrue(os.path.exists(root3))
824+
825+class TestWorkingTreeDiff(TestExtDiffBase):
826+ def setUp(self):
827+ TestExtDiffBase.setUp(self)
828+
829+ self.build_tree(['tree/dir1/', 'tree/dir2/'])
830+ self.build_tree_contents([
831+ ('tree/a', "a"),
832+ ('tree/dir1/b', "b"),
833+ ('tree/dir1/c', "c"),
834+ ('tree/dir1/d', "d"),
835+ ('tree/dir2/e', "e"),
836+ ('tree/dir2/f', "f"),
837+ ])
838+ self.tree.add(["a", "dir1", "dir2",
839+ "dir1/b", "dir1/c", "dir1/d",
840+ "dir2/e", "dir2/f"])
841+ self.tree.commit(message='1')
842+ self.build_tree_contents([
843+ ('tree/a', "A"),
844+ ('tree/dir1/b', "B"),
845+ ('tree/dir1/c', "C"),
846+ ('tree/dir2/e', "E"),
847+ ])
848+ self.ctx.setup("diff.exe", self.tree.basis_tree(), self.tree)
849+
850+ def assertPopen(self, paths, old_contents):
851+ self.assertEqual(self.popen_mock.count, len(paths))
852+
853+ for args, path, old_content in zip(self.popen_mock.args,
854+ paths, old_contents):
855+ tool, old_path, new_path = args[0][0]
856+ self.assertEqual(tool, "diff.exe")
857+ self.assertFileContent(old_path, old_content)
858+ self.assertEqual(new_path, self.tree.abspath(path))
859+
860+ def test_diff_ids(self):
861+ paths = ['a', 'dir1/b']
862+ self.ctx.diff_ids([self.tree.path2id(p) for p in paths])
863+ self.assertPopen(paths, ["a", "b"])
864+
865+ def test_diff_paths(self):
866+ paths = ['a', 'dir1/b']
867+ self.ctx.diff_paths(paths)
868+ self.assertPopen(paths, ["a", "b"])
869+
870+ def test_diff_paths_for_dir(self):
871+ paths = ['dir1/b', 'dir1/c']
872+ self.ctx.diff_paths(['dir1'])
873+ self.assertPopen(paths, ["b", "c"])
874+
875+ def test_diff_tree(self):
876+ paths = ['a', 'dir1/b', 'dir1/c', 'dir2/e']
877+ old_contents = ['a', 'b', 'c', 'e']
878+ self.ctx.diff_tree()
879+ self.assertPopen(paths, old_contents)
880+
881+ def test_diff_tree_for_dir(self):
882+ paths = ['dir1/b', 'dir1/c']
883+ self.ctx.diff_tree(specific_files=['dir1'])
884+ self.assertPopen(paths, ["b", "c"])
885+
886+ def test_diff_for_renamed_files(self):
887+ self.tree.rename_one('a', 'a2')
888+ paths = ['a2']
889+ self.ctx.diff_paths(['a2'])
890+ self.assertPopen(paths, ["a"])
891+
892+ def test_skip_added_file(self):
893+ self.build_tree_contents([
894+ ('tree/a2', "a"),
895+ ])
896+ self.tree.add(['a2'])
897+ self.ctx.diff_paths(['a2'])
898+ self.assertPopen([], [])
899+
900+
901+ def test_skip_removed_file_1(self):
902+ self.tree.remove(['a'], keep_files=True)
903+ self.ctx.diff_paths(['a'])
904+ self.assertPopen([], [])
905+
906+ def test_skip_removed_file_2(self):
907+ self.tree.remove(['a'], keep_files=False)
908+ self.ctx.diff_paths(['a'])
909+ self.assertPopen([], [])
910+
911+if __name__=='__main__':
912+ import unittest
913+ unittest.main()
914+
915
916=== modified file 'lib/treewidget.py'
917--- lib/treewidget.py 2012-01-31 13:30:35 +0000
918+++ lib/treewidget.py 2012-02-24 14:57:59 +0000
919@@ -1892,8 +1892,8 @@
920 self.tree.branch, self.tree.branch,
921 specific_files=paths)
922
923- show_diff(arg_provider, ext_diff=ext_diff,
924- parent_window=self.window())
925+ show_diff(arg_provider, ext_diff=ext_diff, parent_window=self.window(),
926+ context=self.diff_context)
927
928 def unversioned_parents_paths(self, item, include_item=True):
929 paths = []
930
931=== modified file 'lib/widgets/shelvelist.py'
932--- lib/widgets/shelvelist.py 2012-02-08 15:28:46 +0000
933+++ lib/widgets/shelvelist.py 2012-02-24 14:57:59 +0000
934@@ -333,19 +333,8 @@
935
936 def load_diff(self, tree, base_tree):
937 self.file_view.clear()
938-
939- changes = tree.iter_changes(base_tree)
940-
941- def changes_key(change):
942- return change[1][1] or change[1][0]
943
944- for (file_id, paths, changed_content, versioned, parent,
945- name, kind, executable) in sorted(changes, key=changes_key):
946- di = DiffItem.create([base_tree, tree], file_id, paths, changed_content,
947- versioned, parent, name, kind, executable)
948- if not di:
949- continue
950-
951+ for di in DiffItem.iter_items((base_tree, tree), lock_trees=False):
952 di.load()
953
954 old_path, new_path = di.paths

Subscribers

People subscribed via source and target branches