Merge lp:~abentley/bzr/annotate-revspec into lp:bzr

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 5443
Proposed branch: lp:~abentley/bzr/annotate-revspec
Merge into: lp:bzr
Prerequisite: lp:~abentley/bzr/mainline-revspec
Diff against target: 136 lines (+104/-0)
2 files modified
bzrlib/revisionspec.py (+45/-0)
bzrlib/tests/test_revisionspec.py (+59/-0)
To merge this branch: bzr merge lp:~abentley/bzr/annotate-revspec
Reviewer Review Type Date Requested Status
Martin Pool Approve
Review via email: mp+34681@code.launchpad.net

Commit message

implement "annotate" revision spec

Description of the change

This branch builds on the mainline-revspec branch and implements the "annotate"
revspec.

The annotate revspec takes path:line as its input, and selects the revision
that introduced the line.

For example: "bzr log -vp -r annotate:bzrlib/transform.py:500" will select the
revision that introduced line 500 of transform.py, and display its log, status
output and diff.

It can be combined with mainline to select the revision that landed this line
into trunk, like so: bzr log -vp -r mainline:annotate:bzrlib/transform.py:500

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

(feed-pqm now automatically adds the author to the commit message, so I removed it.)

Wow, that's quite nice. This should definitely be in news and whatsnew too.

It's 'existent' not 'existant' <http://en.wiktionary.org/wiki/existant>

tweak

review: Approve
Revision history for this message
Andrew Bennetts (spiv) wrote :

I'll make a branch with NEWS and What's New for this, and do Martin's spelling fix, and submit that.

Revision history for this message
Andrew Bennetts (spiv) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/revisionspec.py'
--- bzrlib/revisionspec.py 2010-09-06 16:07:30 +0000
+++ bzrlib/revisionspec.py 2010-09-06 16:07:31 +0000
@@ -31,6 +31,7 @@
31 revision,31 revision,
32 symbol_versioning,32 symbol_versioning,
33 trace,33 trace,
34 workingtree,
34 )35 )
3536
3637
@@ -900,6 +901,49 @@
900 self._get_submit_location(context_branch))901 self._get_submit_location(context_branch))
901902
902903
904class RevisionSpec_annotate(RevisionIDSpec):
905
906 prefix = 'annotate:'
907
908 help_txt = """Select the revision that last modified the specified line.
909
910 Select the revision that last modified the specified line. Line is
911 specified as path:number. Path is a relative path to the file. Numbers
912 start at 1, and are relative to the current version, not the last-
913 committed version of the file.
914 """
915
916 def _raise_invalid(self, numstring, context_branch):
917 raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
918 'No such line: %s' % numstring)
919
920 def _as_revision_id(self, context_branch):
921 path, numstring = self.spec.rsplit(':', 1)
922 try:
923 index = int(numstring) - 1
924 except ValueError:
925 self._raise_invalid(numstring, context_branch)
926 tree, file_path = workingtree.WorkingTree.open_containing(path)
927 tree.lock_read()
928 try:
929 file_id = tree.path2id(file_path)
930 if file_id is None:
931 raise errors.InvalidRevisionSpec(self.user_spec,
932 context_branch, "File '%s' is not versioned." %
933 file_path)
934 revision_ids = [r for (r, l) in tree.annotate_iter(file_id)]
935 finally:
936 tree.unlock()
937 try:
938 revision_id = revision_ids[index]
939 except IndexError:
940 self._raise_invalid(numstring, context_branch)
941 if revision_id == revision.CURRENT_REVISION:
942 raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
943 'Line %s has not been committed.' % numstring)
944 return revision_id
945
946
903class RevisionSpec_mainline(RevisionIDSpec):947class RevisionSpec_mainline(RevisionIDSpec):
904948
905 help_txt = """Select mainline revision that merged the specified revision.949 help_txt = """Select mainline revision that merged the specified revision.
@@ -948,6 +992,7 @@
948_register_revspec(RevisionSpec_ancestor)992_register_revspec(RevisionSpec_ancestor)
949_register_revspec(RevisionSpec_branch)993_register_revspec(RevisionSpec_branch)
950_register_revspec(RevisionSpec_submit)994_register_revspec(RevisionSpec_submit)
995_register_revspec(RevisionSpec_annotate)
951_register_revspec(RevisionSpec_mainline)996_register_revspec(RevisionSpec_mainline)
952997
953# classes in this list should have a "prefix" attribute, against which998# classes in this list should have a "prefix" attribute, against which
954999
=== modified file 'bzrlib/tests/test_revisionspec.py'
--- bzrlib/tests/test_revisionspec.py 2010-09-06 16:07:30 +0000
+++ bzrlib/tests/test_revisionspec.py 2010-09-06 16:07:31 +0000
@@ -669,3 +669,62 @@
669669
670 def test_in_history(self):670 def test_in_history(self):
671 self.assertInHistoryIs(2, 'r2', 'mainline:revid:alt_r2')671 self.assertInHistoryIs(2, 'r2', 'mainline:revid:alt_r2')
672
673
674class TestRevisionSpec_annotate(TestRevisionSpec):
675
676 def setUp(self):
677 TestRevisionSpec.setUp(self)
678 self.tree = self.make_branch_and_tree('annotate-tree')
679 self.build_tree_contents([('annotate-tree/file1', '1\n')])
680 self.tree.add('file1')
681 self.tree.commit('r1', rev_id='r1')
682 self.build_tree_contents([('annotate-tree/file1', '2\n1\n')])
683 self.tree.commit('r2', rev_id='r2')
684 self.build_tree_contents([('annotate-tree/file1', '2\n1\n3\n')])
685
686 def test_as_revision_id_r1(self):
687 self.assertAsRevisionId('r1', 'annotate:annotate-tree/file1:2')
688
689 def test_as_revision_id_r2(self):
690 self.assertAsRevisionId('r2', 'annotate:annotate-tree/file1:1')
691
692 def test_as_revision_id_uncommitted(self):
693 spec = RevisionSpec.from_string('annotate:annotate-tree/file1:3')
694 e = self.assertRaises(errors.InvalidRevisionSpec,
695 spec.as_revision_id, self.tree.branch)
696 self.assertContainsRe(str(e),
697 r"Requested revision: \'annotate:annotate-tree/file1:3\' does not"
698 " exist in branch: .*\nLine 3 has not been committed.")
699
700 def test_non_existant_line(self):
701 spec = RevisionSpec.from_string('annotate:annotate-tree/file1:4')
702 e = self.assertRaises(errors.InvalidRevisionSpec,
703 spec.as_revision_id, self.tree.branch)
704 self.assertContainsRe(str(e),
705 r"Requested revision: \'annotate:annotate-tree/file1:4\' does not"
706 " exist in branch: .*\nNo such line: 4")
707
708 def test_invalid_line(self):
709 spec = RevisionSpec.from_string('annotate:annotate-tree/file1:q')
710 e = self.assertRaises(errors.InvalidRevisionSpec,
711 spec.as_revision_id, self.tree.branch)
712 self.assertContainsRe(str(e),
713 r"Requested revision: \'annotate:annotate-tree/file1:q\' does not"
714 " exist in branch: .*\nNo such line: q")
715
716 def test_no_such_file(self):
717 spec = RevisionSpec.from_string('annotate:annotate-tree/file2:1')
718 e = self.assertRaises(errors.InvalidRevisionSpec,
719 spec.as_revision_id, self.tree.branch)
720 self.assertContainsRe(str(e),
721 r"Requested revision: \'annotate:annotate-tree/file2:1\' does not"
722 " exist in branch: .*\nFile 'file2' is not versioned")
723
724 def test_no_such_file_with_colon(self):
725 spec = RevisionSpec.from_string('annotate:annotate-tree/fi:le2:1')
726 e = self.assertRaises(errors.InvalidRevisionSpec,
727 spec.as_revision_id, self.tree.branch)
728 self.assertContainsRe(str(e),
729 r"Requested revision: \'annotate:annotate-tree/fi:le2:1\' does not"
730 " exist in branch: .*\nFile 'fi:le2' is not versioned")