Merge lp:~baztian/bzr/recursive-diff into lp:bzr/2.5

Proposed by Bastian
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
Reviewer Review Type Date Requested Status
Vincent Ladeuil Needs Resubmitting
Review via email: mp+97265@code.launchpad.net

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...

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

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?

Revision history for this message
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_recursive_files) is likely
to break without more testing. That's just a gut feeling though :-/

By the way, are you sure 'meld' can't display symlinks ?

_write_recursive_file, as a name, sounds funny and does not seem to match
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.

assertExtractedFiles would be more idiomatic.

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.islink(filepath):

Someone knowing windows better than me can comment about whether this will
work or fail there ?

314 + def test_finish(self):

test_finish_cleans_up_temp_files ?

332 + def test_finish(self):

'Eager Test' (http://xunitpatterns.com/Obscure%20Test.html) is bad, you shouldn't do that :) 'Defect Localization' (http://xunitpatterns.com/Goals%20of%20Test%20Automation.html#Defect%20Localization) is good, do it !

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 ;)

review: Needs Resubmitting
Revision history for this message
Martin Packman (gz) wrote :

Rejecting for now, please resubmit against lp:bzr if you get the chance.

Unmerged revisions

6261. By Bastian

Show diffs using a custom diff program that supports directory based diff

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2012-02-16 16:42:43 +0000
+++ bzrlib/builtins.py 2012-03-13 17:48:37 +0000
@@ -2276,6 +2276,12 @@
2276 Show the differences using a custom diff program with options::2276 Show the differences using a custom diff program with options::
2277 2277
2278 bzr diff --using /usr/bin/diff --diff-options -wu2278 bzr diff --using /usr/bin/diff --diff-options -wu
2279
2280 Show the differences using a custom diff program that supports
2281 directory based diff::
2282
2283 bzr diff --using meld --diff-options @recursive
2284
2279 """2285 """
2280 _see_also = ['status']2286 _see_also = ['status']
2281 takes_args = ['file*']2287 takes_args = ['file*']
22822288
=== modified file 'bzrlib/diff.py'
--- bzrlib/diff.py 2011-12-18 12:46:49 +0000
+++ bzrlib/diff.py 2012-03-13 17:48:37 +0000
@@ -685,20 +685,26 @@
685685
686class DiffFromTool(DiffPath):686class DiffFromTool(DiffPath):
687687
688 _show_diff = False
689
688 def __init__(self, command_template, old_tree, new_tree, to_file,690 def __init__(self, command_template, old_tree, new_tree, to_file,
689 path_encoding='utf-8'):691 path_encoding='utf-8', recursive=False):
690 DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)692 DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
691 self.command_template = command_template693 self.command_template = command_template
694 self._recursive = recursive
692 self._root = osutils.mkdtemp(prefix='bzr-diff-')695 self._root = osutils.mkdtemp(prefix='bzr-diff-')
693696
694 @classmethod697 @classmethod
695 def from_string(klass, command_string, old_tree, new_tree, to_file,698 def from_string(klass, command_string, old_tree, new_tree, to_file,
696 path_encoding='utf-8'):699 path_encoding='utf-8'):
697 command_template = cmdline.split(command_string)700 command_template = cmdline.split(command_string)
698 if '@' not in command_string:701 recursive = '@recursive' in command_template
702 if recursive:
703 command_template.remove('@recursive')
704 if set(command_template).isdisjoint(('@old_path', '@new_path')):
699 command_template.extend(['@old_path', '@new_path'])705 command_template.extend(['@old_path', '@new_path'])
700 return klass(command_template, old_tree, new_tree, to_file,706 return klass(command_template, old_tree, new_tree, to_file,
701 path_encoding)707 path_encoding, recursive)
702708
703 @classmethod709 @classmethod
704 def make_from_diff_tree(klass, command_string, external_diff_options=None):710 def make_from_diff_tree(klass, command_string, external_diff_options=None):
@@ -712,7 +718,7 @@
712718
713 def _get_command(self, old_path, new_path):719 def _get_command(self, old_path, new_path):
714 my_map = {'old_path': old_path, 'new_path': new_path}720 my_map = {'old_path': old_path, 'new_path': new_path}
715 command = [AtTemplate(t).substitute(my_map) for t in721 command = [AtTemplate(t).safe_substitute(my_map) for t in
716 self.command_template]722 self.command_template]
717 if sys.platform == 'win32': # Popen doesn't accept unicode on win32723 if sys.platform == 'win32': # Popen doesn't accept unicode on win32
718 command_encoded = []724 command_encoded = []
@@ -812,6 +818,21 @@
812 osutils.make_readonly(full_path)818 osutils.make_readonly(full_path)
813 return full_path819 return full_path
814820
821 def _write_recursive_file(self, file_id, tree, prefix, relpath, kind):
822 if kind == 'file':
823 return self._write_file(file_id, tree, prefix, relpath,
824 force_temp=True)
825 else:
826 full_path = self._safe_filename(prefix, relpath)
827 if kind == 'directory':
828 dir_path = full_path
829 if not kind:
830 dir_path = osutils.dirname(full_path)
831 try:
832 os.makedirs(dir_path)
833 except OSError:
834 'directory already exists'
835
815 def _prepare_files(self, file_id, old_path, new_path, force_temp=False,836 def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
816 allow_write_new=False):837 allow_write_new=False):
817 old_disk_path = self._write_file(file_id, self.old_tree, 'old',838 old_disk_path = self._write_file(file_id, self.old_tree, 'old',
@@ -822,6 +843,10 @@
822 return old_disk_path, new_disk_path843 return old_disk_path, new_disk_path
823844
824 def finish(self):845 def finish(self):
846 if self._show_diff:
847 old_path = osutils.pathjoin(self._root, 'old')
848 new_path = osutils.pathjoin(self._root, 'new')
849 self._execute(old_path, new_path)
825 try:850 try:
826 osutils.rmtree(self._root)851 osutils.rmtree(self._root)
827 except OSError, e:852 except OSError, e:
@@ -830,11 +855,21 @@
830 "cleanly removed: %s." % (self._root, e))855 "cleanly removed: %s." % (self._root, e))
831856
832 def diff(self, file_id, old_path, new_path, old_kind, new_kind):857 def diff(self, file_id, old_path, new_path, old_kind, new_kind):
833 if (old_kind, new_kind) != ('file', 'file'):858 if self._recursive:
834 return DiffPath.CANNOT_DIFF859 allowed_kinds = ('directory', 'file', None)
835 (old_disk_path, new_disk_path) = self._prepare_files(860 if not (old_kind in allowed_kinds or new_kind in allowed_kinds):
836 file_id, old_path, new_path)861 return self.CANNOT_DIFF
837 self._execute(old_disk_path, new_disk_path)862 self._show_diff = True
863 self._write_recursive_file(file_id, self.old_tree, 'old',
864 old_path, old_kind)
865 self._write_recursive_file(file_id, self.new_tree, 'new',
866 new_path, new_kind)
867 else:
868 if (old_kind, new_kind) != ('file', 'file'):
869 return DiffPath.CANNOT_DIFF
870 (old_disk_path, new_disk_path) = self._prepare_files(
871 file_id, old_path, new_path)
872 self._execute(old_disk_path, new_disk_path)
838873
839 def edit_file(self, file_id):874 def edit_file(self, file_id):
840 """Use this tool to edit a file.875 """Use this tool to edit a file.
841876
=== modified file 'bzrlib/tests/blackbox/test_diff.py'
--- bzrlib/tests/blackbox/test_diff.py 2012-01-05 13:02:31 +0000
+++ bzrlib/tests/blackbox/test_diff.py 2012-03-13 17:48:37 +0000
@@ -26,6 +26,7 @@
26 )26 )
27from bzrlib.diff import (27from bzrlib.diff import (
28 DiffTree,28 DiffTree,
29 DiffFromTool,
29 format_registry as diff_format_registry,30 format_registry as diff_format_registry,
30 )31 )
31from bzrlib.tests import (32from bzrlib.tests import (
@@ -414,6 +415,127 @@
414 self.assertEquals('', err)415 self.assertEquals('', err)
415416
416417
418class TestRecursiveDiff(DiffBase):
419
420 def assert_diff(self, cmd_args, old_files, new_files):
421 """Assert the correct files have been extracted for the
422 revisions to the temp folders.
423 """
424 def execute(other_self, old_path, new_path):
425 self.assert_files(old_path, old_files)
426 self.assert_files(new_path, new_files)
427 self.overrideAttr(DiffFromTool, '_execute', execute)
428 args = ['diff', '--using=foo', '--diff-options=@recursive']
429 args += cmd_args.split()
430 out, err = self.run_bzr(args, retcode=1)
431
432 def assert_files(self, base_path, files):
433 result = []
434 for (dirpath, dirnames, filenames) in os.walk(base_path):
435 dir = os.path.relpath(dirpath, base_path)
436 if not dirnames and not filenames:
437 # empty dir
438 if dirpath != base_path:
439 result.append("%s/" % dir)
440 continue
441 for i in filenames + dirnames:
442 filepath = os.path.join(dirpath, i)
443 if os.path.islink(filepath):
444 dest = os.path.join(dir, i)
445 dest = os.path.relpath(dest)
446 real_source = os.path.realpath(filepath)
447 source = os.path.relpath(real_source, base_path)
448 if os.path.isdir(filepath):
449 source = "%s/" % source
450 result.append((dest, source))
451 elif os.path.isfile(filepath):
452 filename = os.path.join(dir, i)
453 filename = os.path.relpath(filename)
454 result.append(filename)
455 result = sorted(result)
456 expected = sorted(files)
457 self.assertEqual(result, expected)
458
459 def test_file_modified(self):
460 tree = self.make_branch_and_tree('test')
461 self.build_tree(('test/test.txt',))
462 tree.add('test.txt')
463 tree.commit('commit the tree')
464 self.build_tree_contents((('test/test.txt', 'changed file content'),))
465 tree.commit('file changed')
466 self.assert_diff('test -r 1..2', ('test.txt',), ('test.txt',))
467
468 def test_file_modified_and_removed(self):
469 tree = self.make_branch_and_tree('test')
470 self.build_tree(('test/test.txt',))
471 tree.add('test.txt')
472 tree.commit('commit the tree')
473 self.build_tree_contents((('test/test.txt', 'changed file content'),))
474 tree.commit('file changed')
475 tree.remove('test.txt')
476 tree.commit('file removed')
477 self.assert_diff('test -r 1..3', ('test.txt',), ())
478
479 def test_file_added(self):
480 tree = self.make_branch_and_tree('test')
481 self.build_tree(('test/test.txt',))
482 tree.add('test.txt')
483 tree.commit('commit the tree')
484 self.build_tree(('test/test2.txt',))
485 tree.add('test2.txt')
486 tree.commit('commit the tree 2')
487 self.assert_diff('test -r 1..2', (), ('test2.txt',))
488
489 def test_file_removed(self):
490 tree = self.make_branch_and_tree('test')
491 self.build_tree(('test/test.txt',))
492 tree.add('test.txt')
493 tree.commit('commit the tree')
494 tree.remove('test.txt')
495 tree.commit('commit the tree 2')
496 self.assert_diff('test -r 1..2', ('test.txt',), ())
497
498 def test_file_renamed(self):
499 tree = self.make_branch_and_tree('test')
500 self.build_tree(('test/test.txt',))
501 tree.add('test.txt')
502 tree.commit('commit the tree')
503 tree.rename_one('test.txt', 'test2.txt')
504 tree.commit('commit the tree 2')
505 self.assert_diff('test -r 1..2', ('test.txt',), ('test2.txt',))
506
507 def test_dir_renamed(self):
508 tree = self.make_branch_and_tree('test')
509 self.build_tree(('test/test1/', 'test/test1/test.txt',))
510 tree.add('test1')
511 tree.commit('commit the tree')
512 tree.rename_one('test1', 'test2')
513 tree.commit('commit the tree 2')
514 self.assert_diff('test -r 1..2', ('test1/',), ('test2/',))
515
516 def test_branches_with_working_tree(self):
517 tree1 = self.make_branch_and_tree('test1')
518 self.build_tree(('test1/test.txt',))
519 tree1.add('test.txt')
520 tree1.commit('commit the tree')
521 tree2 = tree1.bzrdir.sprout('test2').open_workingtree()
522 self.build_tree(('test2/test2.txt',))
523 tree2.add('test2.txt')
524 tree2.commit('commit the tree2')
525 self.assert_diff('test1 --new test2', (), ('test2.txt',))
526
527 def test_branches_branch2_without_working_tree(self):
528 tree1 = self.make_branch_and_tree('test1')
529 self.build_tree(('test1/test.txt',))
530 tree1.add('test.txt')
531 tree1.commit('commit the tree')
532 tree1.bzrdir.sprout('test2', create_tree_if_local=False)
533 self.build_tree(('test1/test2.txt',))
534 tree1.add('test2.txt')
535 tree1.commit('commit the tree2')
536 self.assert_diff('test1 --new test2', ('test2.txt',), ())
537
538
417class TestDiffOutput(DiffBase):539class TestDiffOutput(DiffBase):
418540
419 def test_diff_output(self):541 def test_diff_output(self):
420542
=== modified file 'bzrlib/tests/test_diff.py'
--- bzrlib/tests/test_diff.py 2012-01-25 21:13:15 +0000
+++ bzrlib/tests/test_diff.py 2012-03-13 17:48:37 +0000
@@ -1319,6 +1319,45 @@
1319 self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],1319 self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
1320 diff_obj._get_command('old-path', 'new-path'))1320 diff_obj._get_command('old-path', 'new-path'))
13211321
1322 def test_from_string_extra_matches(self):
1323 diff_obj = diff.DiffFromTool.from_string('diff @ignoreme',
1324 None, None, None)
1325 self.addCleanup(diff_obj.finish)
1326 self.assertEqual(['diff', '@ignoreme', '@old_path', '@new_path'],
1327 diff_obj.command_template)
1328 self.assertEqual(['diff', '@ignoreme', 'old-path', 'new-path'],
1329 diff_obj._get_command('old-path', 'new-path'))
1330
1331 def _pre_make_from_diff_tree(self, *args):
1332 class DummyDiffTree(object):
1333 old_tree = "old_tree"
1334 new_tree = "new_tree"
1335 to_file = "to_file"
1336 diff_obj = diff.DiffFromTool.make_from_diff_tree(*args)(DummyDiffTree())
1337 self.addCleanup(diff_obj.finish)
1338 self.assertEqual(DummyDiffTree.old_tree, diff_obj.old_tree)
1339 self.assertEqual(DummyDiffTree.new_tree, diff_obj.new_tree)
1340 self.assertEqual(DummyDiffTree.to_file, diff_obj.to_file)
1341 return diff_obj
1342
1343 def test_make_from_diff_tree(self):
1344 diff_obj = self._pre_make_from_diff_tree('diff')
1345 self.assertEqual(['diff', '@old_path', '@new_path'],
1346 diff_obj.command_template)
1347 self.assertFalse(diff_obj._recursive)
1348
1349 def test_make_from_diff_tree_no_path_but_extra_option(self):
1350 diff_obj = self._pre_make_from_diff_tree('diff', 'def')
1351 self.assertEqual(['diff', 'def', '@old_path', '@new_path'],
1352 diff_obj.command_template)
1353 self.assertFalse(diff_obj._recursive)
1354
1355 def test_make_from_diff_tree_recursive(self):
1356 diff_obj = self._pre_make_from_diff_tree('diff', '@recursive')
1357 self.assertEqual(['diff', '@old_path', '@new_path'],
1358 diff_obj.command_template)
1359 self.assertTrue(diff_obj._recursive)
1360
1322 def test_execute(self):1361 def test_execute(self):
1323 output = StringIO()1362 output = StringIO()
1324 diff_obj = diff.DiffFromTool(['python', '-c',1363 diff_obj = diff.DiffFromTool(['python', '-c',
@@ -1328,6 +1367,15 @@
1328 diff_obj._execute('old', 'new')1367 diff_obj._execute('old', 'new')
1329 self.assertEqual(output.getvalue().rstrip(), 'old new')1368 self.assertEqual(output.getvalue().rstrip(), 'old new')
13301369
1370 def test_finish(self):
1371 output = StringIO()
1372 diff_obj = diff.DiffFromTool(['python', '-c',
1373 'print "@old_path @new_path"'],
1374 None, None, output)
1375 self.assertTrue(os.path.exists(diff_obj._root))
1376 diff_obj.finish()
1377 self.assertFalse(os.path.exists(diff_obj._root))
1378
1331 def test_excute_missing(self):1379 def test_excute_missing(self):
1332 diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],1380 diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
1333 None, None, None)1381 None, None, None)
@@ -1403,6 +1451,60 @@
1403 diff_obj._prepare_files('file2-id', 'oldname2', 'newname2')1451 diff_obj._prepare_files('file2-id', 'oldname2', 'newname2')
14041452
14051453
1454class TestDiffFromToolRecursive(tests.TestCaseWithTransport):
1455
1456 def test_finish(self):
1457 self.old_tree = self.make_branch_and_tree('old-tree')
1458 self.old_tree.lock_write()
1459 self.addCleanup(self.old_tree.unlock)
1460 self.new_tree = self.make_branch_and_tree('new-tree')
1461 self.new_tree.lock_write()
1462 self.addCleanup(self.new_tree.unlock)
1463 self.build_tree_contents([('old-tree/olddir/',),
1464 ('old-tree/olddir/oldfile', 'old\n'),
1465 ('old-tree/oldfile2', 'old2\n')])
1466 self.old_tree.add('olddir')
1467 self.old_tree.add('olddir/oldfile', 'file-id')
1468 self.old_tree.add('oldfile2', 'file-id2')
1469 self.build_tree_contents([('new-tree/newdir/',),
1470 ('new-tree/newdir/newfile', 'new\n'),
1471 ('new-tree/newfile2', 'new2\n')])
1472 self.new_tree.add('newdir')
1473 self.new_tree.add('newdir/newfile', 'file-id')
1474 self.new_tree.add('newfile2', 'file-id2')
1475
1476 output = StringIO()
1477 diff_obj = diff.DiffFromTool(['python', '-c',
1478 'print "Foo @old_path @new_path"'],
1479 self.old_tree, self.new_tree,
1480 output, recursive=True)
1481 old_tmp_dir = os.path.join(diff_obj._root, 'old')
1482 new_tmp_dir = os.path.join(diff_obj._root, 'new')
1483
1484 diff_obj.diff('file-id', 'olddir/oldfile', 'newdir/newfile',
1485 'file', 'file')
1486 self.assertDirContents(old_tmp_dir, ['olddir'])
1487 self.assertDirContents(new_tmp_dir, ['newdir'])
1488 self.assertDirContents(os.path.join(old_tmp_dir, 'olddir'),
1489 ['oldfile'])
1490 self.assertDirContents(os.path.join(new_tmp_dir, 'newdir'),
1491 ['newfile'])
1492
1493 diff_obj.diff('file-id2', 'oldfile2', 'newfile2', 'file', 'file')
1494 self.assertDirContents(old_tmp_dir, ['olddir', 'oldfile2'])
1495 self.assertDirContents(new_tmp_dir, ['newdir', 'newfile2'])
1496 self.assertEqual(output.getvalue(), '')
1497 diff_obj.finish()
1498 self.assertEqual(output.getvalue().rstrip(),
1499 'Foo %s %s' % (old_tmp_dir, new_tmp_dir))
1500 self.assertFalse(os.path.exists(old_tmp_dir))
1501 self.assertFalse(os.path.exists(new_tmp_dir))
1502
1503 def assertDirContents(self, old_tmp_dir, contents):
1504 old_list = sorted(os.listdir(old_tmp_dir))
1505 self.assertEqual(contents, old_list)
1506
1507
1406class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):1508class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
14071509
1408 def test_encodable_filename(self):1510 def test_encodable_filename(self):
14091511
=== modified file 'doc/en/release-notes/bzr-2.5.txt'
--- doc/en/release-notes/bzr-2.5.txt 2012-03-12 18:34:04 +0000
+++ doc/en/release-notes/bzr-2.5.txt 2012-03-13 17:48:37 +0000
@@ -18,7 +18,10 @@
18New Features18New Features
19************19************
2020
21.. New commands, options, etc that users may wish to try out.21* If your external diff tool supports a directory based diff
22 (recursive diff) you can now specify the @recursive diff
23 option. This avoids opening a diff tool instance for every differing
24 file. (Bastian Bowe)
2225
23Improvements26Improvements
24************27************

Subscribers

People subscribed via source and target branches