Merge lp:~hid-iwata/qbzr/extdiff-improve into lp:qbzr
- extdiff-improve
- Merge into trunk2a
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alexander Belchenko | Approve | ||
Review via email: mp+94326@code.launchpad.net |
Commit message
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.
- 1468. By IWATA Hidetaka
-
fix wrong comment.
Alexander Belchenko (bialix) wrote : | # |
- 1469. By IWATA Hidetaka
-
merge from trunk (start 0.23)
- 1470. By IWATA Hidetaka
-
update NEWS
IWATA Hidetaka (hid-iwata) wrote : | # |
OK. I've update NEWS.txt for 0.23.
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(
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_
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.
- 1471. By IWATA Hidetaka
-
Change argument order of show_diff (To avoid compatibility break)
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.
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.
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:/
> You are the owner of lp:~hid-iwata/qbzr/extdiff-improve.
Preview Diff
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 |
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