Merge lp:~baztian/bzr/recursive-diff into lp:bzr/2.5
- recursive-diff
- Merge into 2.5
Status: | Rejected |
---|---|
Rejected by: | Martin Packman |
Proposed branch: | lp:~baztian/bzr/recursive-diff |
Merge into: | lp:bzr/2.5 |
Diff against target: |
402 lines (+278/-10) 5 files modified
bzrlib/builtins.py (+6/-0) bzrlib/diff.py (+44/-9) bzrlib/tests/blackbox/test_diff.py (+122/-0) bzrlib/tests/test_diff.py (+102/-0) doc/en/release-notes/bzr-2.5.txt (+4/-1) |
To merge this branch: | bzr merge lp:~baztian/bzr/recursive-diff |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Vincent Ladeuil | Needs Resubmitting | ||
Review via email: mp+97265@code.launchpad.net |
Commit message
Description of the change
Support recursive diff: Show the differences using a custom diff program that supports directory based diff. I would love to have this in 2.5 and 2.6...
Martin Packman (gz) wrote : | # |
Vincent Ladeuil (vila) wrote : | # |
/me nods@mgz about 2.5 being closed for new features.
As 'meld' can already be used without the @recursive flag, I understand why
you put it in the --diff-options, but I too feel it weird. I think it would
be better to have a dedicated option that can be used *only* when --using is
also used, not sure about which name to use but since diff is already
recursive, may be --tree ?
Overall this is a nice feature to have and I'm glad you're working on it. I
wish we have a way to test it better than a review alone allows as I
suspect ignoring stuff you can't diff (in _write_
to break without more testing. That's just a gut feeling though :-/
By the way, are you sure 'meld' can't display symlinks ?
_write_
the design.
It seems to me that you want to change DiffTree (or rather subclass it or
something) instead of DiffPath. I.e. really diff trees rather than hacking
how one file is diff'ed.
138 + def assert_diff(self, cmd_args, old_files, new_files):
139 + """Assert the correct files have been extracted for the
140 + revisions to the temp folders.
assertExtracted
150 + def assert_files(self, base_path, files):
Wow, this does a lot ! Worth a docstring and comments ;)
Should it be named assertProducedFiles or something ?
This seems to be a nice set of tests, but they are a bit hard too read. For
one, they seem to duplicate some setup that could go into a ... setUp()
method ;)
You may also want to add tests for symlinks (requiring features.Symlink) if only to check and document how your change behaves.
161 + if os.path.
Someone knowing windows better than me can comment about whether this will
work or fail there ?
314 + def test_finish(self):
test_finish_
332 + def test_finish(self):
'Eager Test' (http://
Can you split it up with meaningful names ?
Please re-target to 2.6 and let's keep the discussion going, this really
sounds like a very cool feature ;)
Martin Packman (gz) wrote : | # |
Rejecting for now, please resubmit against lp:bzr if you get the chance.
Preview Diff
1 | === modified file 'bzrlib/builtins.py' |
2 | --- bzrlib/builtins.py 2012-02-16 16:42:43 +0000 |
3 | +++ bzrlib/builtins.py 2012-03-13 17:48:37 +0000 |
4 | @@ -2276,6 +2276,12 @@ |
5 | Show the differences using a custom diff program with options:: |
6 | |
7 | bzr diff --using /usr/bin/diff --diff-options -wu |
8 | + |
9 | + Show the differences using a custom diff program that supports |
10 | + directory based diff:: |
11 | + |
12 | + bzr diff --using meld --diff-options @recursive |
13 | + |
14 | """ |
15 | _see_also = ['status'] |
16 | takes_args = ['file*'] |
17 | |
18 | === modified file 'bzrlib/diff.py' |
19 | --- bzrlib/diff.py 2011-12-18 12:46:49 +0000 |
20 | +++ bzrlib/diff.py 2012-03-13 17:48:37 +0000 |
21 | @@ -685,20 +685,26 @@ |
22 | |
23 | class DiffFromTool(DiffPath): |
24 | |
25 | + _show_diff = False |
26 | + |
27 | def __init__(self, command_template, old_tree, new_tree, to_file, |
28 | - path_encoding='utf-8'): |
29 | + path_encoding='utf-8', recursive=False): |
30 | DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding) |
31 | self.command_template = command_template |
32 | + self._recursive = recursive |
33 | self._root = osutils.mkdtemp(prefix='bzr-diff-') |
34 | |
35 | @classmethod |
36 | def from_string(klass, command_string, old_tree, new_tree, to_file, |
37 | path_encoding='utf-8'): |
38 | command_template = cmdline.split(command_string) |
39 | - if '@' not in command_string: |
40 | + recursive = '@recursive' in command_template |
41 | + if recursive: |
42 | + command_template.remove('@recursive') |
43 | + if set(command_template).isdisjoint(('@old_path', '@new_path')): |
44 | command_template.extend(['@old_path', '@new_path']) |
45 | return klass(command_template, old_tree, new_tree, to_file, |
46 | - path_encoding) |
47 | + path_encoding, recursive) |
48 | |
49 | @classmethod |
50 | def make_from_diff_tree(klass, command_string, external_diff_options=None): |
51 | @@ -712,7 +718,7 @@ |
52 | |
53 | def _get_command(self, old_path, new_path): |
54 | my_map = {'old_path': old_path, 'new_path': new_path} |
55 | - command = [AtTemplate(t).substitute(my_map) for t in |
56 | + command = [AtTemplate(t).safe_substitute(my_map) for t in |
57 | self.command_template] |
58 | if sys.platform == 'win32': # Popen doesn't accept unicode on win32 |
59 | command_encoded = [] |
60 | @@ -812,6 +818,21 @@ |
61 | osutils.make_readonly(full_path) |
62 | return full_path |
63 | |
64 | + def _write_recursive_file(self, file_id, tree, prefix, relpath, kind): |
65 | + if kind == 'file': |
66 | + return self._write_file(file_id, tree, prefix, relpath, |
67 | + force_temp=True) |
68 | + else: |
69 | + full_path = self._safe_filename(prefix, relpath) |
70 | + if kind == 'directory': |
71 | + dir_path = full_path |
72 | + if not kind: |
73 | + dir_path = osutils.dirname(full_path) |
74 | + try: |
75 | + os.makedirs(dir_path) |
76 | + except OSError: |
77 | + 'directory already exists' |
78 | + |
79 | def _prepare_files(self, file_id, old_path, new_path, force_temp=False, |
80 | allow_write_new=False): |
81 | old_disk_path = self._write_file(file_id, self.old_tree, 'old', |
82 | @@ -822,6 +843,10 @@ |
83 | return old_disk_path, new_disk_path |
84 | |
85 | def finish(self): |
86 | + if self._show_diff: |
87 | + old_path = osutils.pathjoin(self._root, 'old') |
88 | + new_path = osutils.pathjoin(self._root, 'new') |
89 | + self._execute(old_path, new_path) |
90 | try: |
91 | osutils.rmtree(self._root) |
92 | except OSError, e: |
93 | @@ -830,11 +855,21 @@ |
94 | "cleanly removed: %s." % (self._root, e)) |
95 | |
96 | def diff(self, file_id, old_path, new_path, old_kind, new_kind): |
97 | - if (old_kind, new_kind) != ('file', 'file'): |
98 | - return DiffPath.CANNOT_DIFF |
99 | - (old_disk_path, new_disk_path) = self._prepare_files( |
100 | - file_id, old_path, new_path) |
101 | - self._execute(old_disk_path, new_disk_path) |
102 | + if self._recursive: |
103 | + allowed_kinds = ('directory', 'file', None) |
104 | + if not (old_kind in allowed_kinds or new_kind in allowed_kinds): |
105 | + return self.CANNOT_DIFF |
106 | + self._show_diff = True |
107 | + self._write_recursive_file(file_id, self.old_tree, 'old', |
108 | + old_path, old_kind) |
109 | + self._write_recursive_file(file_id, self.new_tree, 'new', |
110 | + new_path, new_kind) |
111 | + else: |
112 | + if (old_kind, new_kind) != ('file', 'file'): |
113 | + return DiffPath.CANNOT_DIFF |
114 | + (old_disk_path, new_disk_path) = self._prepare_files( |
115 | + file_id, old_path, new_path) |
116 | + self._execute(old_disk_path, new_disk_path) |
117 | |
118 | def edit_file(self, file_id): |
119 | """Use this tool to edit a file. |
120 | |
121 | === modified file 'bzrlib/tests/blackbox/test_diff.py' |
122 | --- bzrlib/tests/blackbox/test_diff.py 2012-01-05 13:02:31 +0000 |
123 | +++ bzrlib/tests/blackbox/test_diff.py 2012-03-13 17:48:37 +0000 |
124 | @@ -26,6 +26,7 @@ |
125 | ) |
126 | from bzrlib.diff import ( |
127 | DiffTree, |
128 | + DiffFromTool, |
129 | format_registry as diff_format_registry, |
130 | ) |
131 | from bzrlib.tests import ( |
132 | @@ -414,6 +415,127 @@ |
133 | self.assertEquals('', err) |
134 | |
135 | |
136 | +class TestRecursiveDiff(DiffBase): |
137 | + |
138 | + def assert_diff(self, cmd_args, old_files, new_files): |
139 | + """Assert the correct files have been extracted for the |
140 | + revisions to the temp folders. |
141 | + """ |
142 | + def execute(other_self, old_path, new_path): |
143 | + self.assert_files(old_path, old_files) |
144 | + self.assert_files(new_path, new_files) |
145 | + self.overrideAttr(DiffFromTool, '_execute', execute) |
146 | + args = ['diff', '--using=foo', '--diff-options=@recursive'] |
147 | + args += cmd_args.split() |
148 | + out, err = self.run_bzr(args, retcode=1) |
149 | + |
150 | + def assert_files(self, base_path, files): |
151 | + result = [] |
152 | + for (dirpath, dirnames, filenames) in os.walk(base_path): |
153 | + dir = os.path.relpath(dirpath, base_path) |
154 | + if not dirnames and not filenames: |
155 | + # empty dir |
156 | + if dirpath != base_path: |
157 | + result.append("%s/" % dir) |
158 | + continue |
159 | + for i in filenames + dirnames: |
160 | + filepath = os.path.join(dirpath, i) |
161 | + if os.path.islink(filepath): |
162 | + dest = os.path.join(dir, i) |
163 | + dest = os.path.relpath(dest) |
164 | + real_source = os.path.realpath(filepath) |
165 | + source = os.path.relpath(real_source, base_path) |
166 | + if os.path.isdir(filepath): |
167 | + source = "%s/" % source |
168 | + result.append((dest, source)) |
169 | + elif os.path.isfile(filepath): |
170 | + filename = os.path.join(dir, i) |
171 | + filename = os.path.relpath(filename) |
172 | + result.append(filename) |
173 | + result = sorted(result) |
174 | + expected = sorted(files) |
175 | + self.assertEqual(result, expected) |
176 | + |
177 | + def test_file_modified(self): |
178 | + tree = self.make_branch_and_tree('test') |
179 | + self.build_tree(('test/test.txt',)) |
180 | + tree.add('test.txt') |
181 | + tree.commit('commit the tree') |
182 | + self.build_tree_contents((('test/test.txt', 'changed file content'),)) |
183 | + tree.commit('file changed') |
184 | + self.assert_diff('test -r 1..2', ('test.txt',), ('test.txt',)) |
185 | + |
186 | + def test_file_modified_and_removed(self): |
187 | + tree = self.make_branch_and_tree('test') |
188 | + self.build_tree(('test/test.txt',)) |
189 | + tree.add('test.txt') |
190 | + tree.commit('commit the tree') |
191 | + self.build_tree_contents((('test/test.txt', 'changed file content'),)) |
192 | + tree.commit('file changed') |
193 | + tree.remove('test.txt') |
194 | + tree.commit('file removed') |
195 | + self.assert_diff('test -r 1..3', ('test.txt',), ()) |
196 | + |
197 | + def test_file_added(self): |
198 | + tree = self.make_branch_and_tree('test') |
199 | + self.build_tree(('test/test.txt',)) |
200 | + tree.add('test.txt') |
201 | + tree.commit('commit the tree') |
202 | + self.build_tree(('test/test2.txt',)) |
203 | + tree.add('test2.txt') |
204 | + tree.commit('commit the tree 2') |
205 | + self.assert_diff('test -r 1..2', (), ('test2.txt',)) |
206 | + |
207 | + def test_file_removed(self): |
208 | + tree = self.make_branch_and_tree('test') |
209 | + self.build_tree(('test/test.txt',)) |
210 | + tree.add('test.txt') |
211 | + tree.commit('commit the tree') |
212 | + tree.remove('test.txt') |
213 | + tree.commit('commit the tree 2') |
214 | + self.assert_diff('test -r 1..2', ('test.txt',), ()) |
215 | + |
216 | + def test_file_renamed(self): |
217 | + tree = self.make_branch_and_tree('test') |
218 | + self.build_tree(('test/test.txt',)) |
219 | + tree.add('test.txt') |
220 | + tree.commit('commit the tree') |
221 | + tree.rename_one('test.txt', 'test2.txt') |
222 | + tree.commit('commit the tree 2') |
223 | + self.assert_diff('test -r 1..2', ('test.txt',), ('test2.txt',)) |
224 | + |
225 | + def test_dir_renamed(self): |
226 | + tree = self.make_branch_and_tree('test') |
227 | + self.build_tree(('test/test1/', 'test/test1/test.txt',)) |
228 | + tree.add('test1') |
229 | + tree.commit('commit the tree') |
230 | + tree.rename_one('test1', 'test2') |
231 | + tree.commit('commit the tree 2') |
232 | + self.assert_diff('test -r 1..2', ('test1/',), ('test2/',)) |
233 | + |
234 | + def test_branches_with_working_tree(self): |
235 | + tree1 = self.make_branch_and_tree('test1') |
236 | + self.build_tree(('test1/test.txt',)) |
237 | + tree1.add('test.txt') |
238 | + tree1.commit('commit the tree') |
239 | + tree2 = tree1.bzrdir.sprout('test2').open_workingtree() |
240 | + self.build_tree(('test2/test2.txt',)) |
241 | + tree2.add('test2.txt') |
242 | + tree2.commit('commit the tree2') |
243 | + self.assert_diff('test1 --new test2', (), ('test2.txt',)) |
244 | + |
245 | + def test_branches_branch2_without_working_tree(self): |
246 | + tree1 = self.make_branch_and_tree('test1') |
247 | + self.build_tree(('test1/test.txt',)) |
248 | + tree1.add('test.txt') |
249 | + tree1.commit('commit the tree') |
250 | + tree1.bzrdir.sprout('test2', create_tree_if_local=False) |
251 | + self.build_tree(('test1/test2.txt',)) |
252 | + tree1.add('test2.txt') |
253 | + tree1.commit('commit the tree2') |
254 | + self.assert_diff('test1 --new test2', ('test2.txt',), ()) |
255 | + |
256 | + |
257 | class TestDiffOutput(DiffBase): |
258 | |
259 | def test_diff_output(self): |
260 | |
261 | === modified file 'bzrlib/tests/test_diff.py' |
262 | --- bzrlib/tests/test_diff.py 2012-01-25 21:13:15 +0000 |
263 | +++ bzrlib/tests/test_diff.py 2012-03-13 17:48:37 +0000 |
264 | @@ -1319,6 +1319,45 @@ |
265 | self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'], |
266 | diff_obj._get_command('old-path', 'new-path')) |
267 | |
268 | + def test_from_string_extra_matches(self): |
269 | + diff_obj = diff.DiffFromTool.from_string('diff @ignoreme', |
270 | + None, None, None) |
271 | + self.addCleanup(diff_obj.finish) |
272 | + self.assertEqual(['diff', '@ignoreme', '@old_path', '@new_path'], |
273 | + diff_obj.command_template) |
274 | + self.assertEqual(['diff', '@ignoreme', 'old-path', 'new-path'], |
275 | + diff_obj._get_command('old-path', 'new-path')) |
276 | + |
277 | + def _pre_make_from_diff_tree(self, *args): |
278 | + class DummyDiffTree(object): |
279 | + old_tree = "old_tree" |
280 | + new_tree = "new_tree" |
281 | + to_file = "to_file" |
282 | + diff_obj = diff.DiffFromTool.make_from_diff_tree(*args)(DummyDiffTree()) |
283 | + self.addCleanup(diff_obj.finish) |
284 | + self.assertEqual(DummyDiffTree.old_tree, diff_obj.old_tree) |
285 | + self.assertEqual(DummyDiffTree.new_tree, diff_obj.new_tree) |
286 | + self.assertEqual(DummyDiffTree.to_file, diff_obj.to_file) |
287 | + return diff_obj |
288 | + |
289 | + def test_make_from_diff_tree(self): |
290 | + diff_obj = self._pre_make_from_diff_tree('diff') |
291 | + self.assertEqual(['diff', '@old_path', '@new_path'], |
292 | + diff_obj.command_template) |
293 | + self.assertFalse(diff_obj._recursive) |
294 | + |
295 | + def test_make_from_diff_tree_no_path_but_extra_option(self): |
296 | + diff_obj = self._pre_make_from_diff_tree('diff', 'def') |
297 | + self.assertEqual(['diff', 'def', '@old_path', '@new_path'], |
298 | + diff_obj.command_template) |
299 | + self.assertFalse(diff_obj._recursive) |
300 | + |
301 | + def test_make_from_diff_tree_recursive(self): |
302 | + diff_obj = self._pre_make_from_diff_tree('diff', '@recursive') |
303 | + self.assertEqual(['diff', '@old_path', '@new_path'], |
304 | + diff_obj.command_template) |
305 | + self.assertTrue(diff_obj._recursive) |
306 | + |
307 | def test_execute(self): |
308 | output = StringIO() |
309 | diff_obj = diff.DiffFromTool(['python', '-c', |
310 | @@ -1328,6 +1367,15 @@ |
311 | diff_obj._execute('old', 'new') |
312 | self.assertEqual(output.getvalue().rstrip(), 'old new') |
313 | |
314 | + def test_finish(self): |
315 | + output = StringIO() |
316 | + diff_obj = diff.DiffFromTool(['python', '-c', |
317 | + 'print "@old_path @new_path"'], |
318 | + None, None, output) |
319 | + self.assertTrue(os.path.exists(diff_obj._root)) |
320 | + diff_obj.finish() |
321 | + self.assertFalse(os.path.exists(diff_obj._root)) |
322 | + |
323 | def test_excute_missing(self): |
324 | diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'], |
325 | None, None, None) |
326 | @@ -1403,6 +1451,60 @@ |
327 | diff_obj._prepare_files('file2-id', 'oldname2', 'newname2') |
328 | |
329 | |
330 | +class TestDiffFromToolRecursive(tests.TestCaseWithTransport): |
331 | + |
332 | + def test_finish(self): |
333 | + self.old_tree = self.make_branch_and_tree('old-tree') |
334 | + self.old_tree.lock_write() |
335 | + self.addCleanup(self.old_tree.unlock) |
336 | + self.new_tree = self.make_branch_and_tree('new-tree') |
337 | + self.new_tree.lock_write() |
338 | + self.addCleanup(self.new_tree.unlock) |
339 | + self.build_tree_contents([('old-tree/olddir/',), |
340 | + ('old-tree/olddir/oldfile', 'old\n'), |
341 | + ('old-tree/oldfile2', 'old2\n')]) |
342 | + self.old_tree.add('olddir') |
343 | + self.old_tree.add('olddir/oldfile', 'file-id') |
344 | + self.old_tree.add('oldfile2', 'file-id2') |
345 | + self.build_tree_contents([('new-tree/newdir/',), |
346 | + ('new-tree/newdir/newfile', 'new\n'), |
347 | + ('new-tree/newfile2', 'new2\n')]) |
348 | + self.new_tree.add('newdir') |
349 | + self.new_tree.add('newdir/newfile', 'file-id') |
350 | + self.new_tree.add('newfile2', 'file-id2') |
351 | + |
352 | + output = StringIO() |
353 | + diff_obj = diff.DiffFromTool(['python', '-c', |
354 | + 'print "Foo @old_path @new_path"'], |
355 | + self.old_tree, self.new_tree, |
356 | + output, recursive=True) |
357 | + old_tmp_dir = os.path.join(diff_obj._root, 'old') |
358 | + new_tmp_dir = os.path.join(diff_obj._root, 'new') |
359 | + |
360 | + diff_obj.diff('file-id', 'olddir/oldfile', 'newdir/newfile', |
361 | + 'file', 'file') |
362 | + self.assertDirContents(old_tmp_dir, ['olddir']) |
363 | + self.assertDirContents(new_tmp_dir, ['newdir']) |
364 | + self.assertDirContents(os.path.join(old_tmp_dir, 'olddir'), |
365 | + ['oldfile']) |
366 | + self.assertDirContents(os.path.join(new_tmp_dir, 'newdir'), |
367 | + ['newfile']) |
368 | + |
369 | + diff_obj.diff('file-id2', 'oldfile2', 'newfile2', 'file', 'file') |
370 | + self.assertDirContents(old_tmp_dir, ['olddir', 'oldfile2']) |
371 | + self.assertDirContents(new_tmp_dir, ['newdir', 'newfile2']) |
372 | + self.assertEqual(output.getvalue(), '') |
373 | + diff_obj.finish() |
374 | + self.assertEqual(output.getvalue().rstrip(), |
375 | + 'Foo %s %s' % (old_tmp_dir, new_tmp_dir)) |
376 | + self.assertFalse(os.path.exists(old_tmp_dir)) |
377 | + self.assertFalse(os.path.exists(new_tmp_dir)) |
378 | + |
379 | + def assertDirContents(self, old_tmp_dir, contents): |
380 | + old_list = sorted(os.listdir(old_tmp_dir)) |
381 | + self.assertEqual(contents, old_list) |
382 | + |
383 | + |
384 | class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport): |
385 | |
386 | def test_encodable_filename(self): |
387 | |
388 | === modified file 'doc/en/release-notes/bzr-2.5.txt' |
389 | --- doc/en/release-notes/bzr-2.5.txt 2012-03-12 18:34:04 +0000 |
390 | +++ doc/en/release-notes/bzr-2.5.txt 2012-03-13 17:48:37 +0000 |
391 | @@ -18,7 +18,10 @@ |
392 | New Features |
393 | ************ |
394 | |
395 | -.. New commands, options, etc that users may wish to try out. |
396 | +* If your external diff tool supports a directory based diff |
397 | + (recursive diff) you can now specify the @recursive diff |
398 | + option. This avoids opening a diff tool instance for every differing |
399 | + file. (Bastian Bowe) |
400 | |
401 | Improvements |
402 | ************ |
This seems like a new feature, so it's too late for 2.5 really.
I'm not sure about using the template-like @recursive to trigger this, it seems instead like it should happen at the command level. Why did you go for that over a flag on the diff command?