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
1=== modified file 'bzrlib/revisionspec.py'
2--- bzrlib/revisionspec.py 2010-09-06 16:07:30 +0000
3+++ bzrlib/revisionspec.py 2010-09-06 16:07:31 +0000
4@@ -31,6 +31,7 @@
5 revision,
6 symbol_versioning,
7 trace,
8+ workingtree,
9 )
10
11
12@@ -900,6 +901,49 @@
13 self._get_submit_location(context_branch))
14
15
16+class RevisionSpec_annotate(RevisionIDSpec):
17+
18+ prefix = 'annotate:'
19+
20+ help_txt = """Select the revision that last modified the specified line.
21+
22+ Select the revision that last modified the specified line. Line is
23+ specified as path:number. Path is a relative path to the file. Numbers
24+ start at 1, and are relative to the current version, not the last-
25+ committed version of the file.
26+ """
27+
28+ def _raise_invalid(self, numstring, context_branch):
29+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
30+ 'No such line: %s' % numstring)
31+
32+ def _as_revision_id(self, context_branch):
33+ path, numstring = self.spec.rsplit(':', 1)
34+ try:
35+ index = int(numstring) - 1
36+ except ValueError:
37+ self._raise_invalid(numstring, context_branch)
38+ tree, file_path = workingtree.WorkingTree.open_containing(path)
39+ tree.lock_read()
40+ try:
41+ file_id = tree.path2id(file_path)
42+ if file_id is None:
43+ raise errors.InvalidRevisionSpec(self.user_spec,
44+ context_branch, "File '%s' is not versioned." %
45+ file_path)
46+ revision_ids = [r for (r, l) in tree.annotate_iter(file_id)]
47+ finally:
48+ tree.unlock()
49+ try:
50+ revision_id = revision_ids[index]
51+ except IndexError:
52+ self._raise_invalid(numstring, context_branch)
53+ if revision_id == revision.CURRENT_REVISION:
54+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
55+ 'Line %s has not been committed.' % numstring)
56+ return revision_id
57+
58+
59 class RevisionSpec_mainline(RevisionIDSpec):
60
61 help_txt = """Select mainline revision that merged the specified revision.
62@@ -948,6 +992,7 @@
63 _register_revspec(RevisionSpec_ancestor)
64 _register_revspec(RevisionSpec_branch)
65 _register_revspec(RevisionSpec_submit)
66+_register_revspec(RevisionSpec_annotate)
67 _register_revspec(RevisionSpec_mainline)
68
69 # classes in this list should have a "prefix" attribute, against which
70
71=== modified file 'bzrlib/tests/test_revisionspec.py'
72--- bzrlib/tests/test_revisionspec.py 2010-09-06 16:07:30 +0000
73+++ bzrlib/tests/test_revisionspec.py 2010-09-06 16:07:31 +0000
74@@ -669,3 +669,62 @@
75
76 def test_in_history(self):
77 self.assertInHistoryIs(2, 'r2', 'mainline:revid:alt_r2')
78+
79+
80+class TestRevisionSpec_annotate(TestRevisionSpec):
81+
82+ def setUp(self):
83+ TestRevisionSpec.setUp(self)
84+ self.tree = self.make_branch_and_tree('annotate-tree')
85+ self.build_tree_contents([('annotate-tree/file1', '1\n')])
86+ self.tree.add('file1')
87+ self.tree.commit('r1', rev_id='r1')
88+ self.build_tree_contents([('annotate-tree/file1', '2\n1\n')])
89+ self.tree.commit('r2', rev_id='r2')
90+ self.build_tree_contents([('annotate-tree/file1', '2\n1\n3\n')])
91+
92+ def test_as_revision_id_r1(self):
93+ self.assertAsRevisionId('r1', 'annotate:annotate-tree/file1:2')
94+
95+ def test_as_revision_id_r2(self):
96+ self.assertAsRevisionId('r2', 'annotate:annotate-tree/file1:1')
97+
98+ def test_as_revision_id_uncommitted(self):
99+ spec = RevisionSpec.from_string('annotate:annotate-tree/file1:3')
100+ e = self.assertRaises(errors.InvalidRevisionSpec,
101+ spec.as_revision_id, self.tree.branch)
102+ self.assertContainsRe(str(e),
103+ r"Requested revision: \'annotate:annotate-tree/file1:3\' does not"
104+ " exist in branch: .*\nLine 3 has not been committed.")
105+
106+ def test_non_existant_line(self):
107+ spec = RevisionSpec.from_string('annotate:annotate-tree/file1:4')
108+ e = self.assertRaises(errors.InvalidRevisionSpec,
109+ spec.as_revision_id, self.tree.branch)
110+ self.assertContainsRe(str(e),
111+ r"Requested revision: \'annotate:annotate-tree/file1:4\' does not"
112+ " exist in branch: .*\nNo such line: 4")
113+
114+ def test_invalid_line(self):
115+ spec = RevisionSpec.from_string('annotate:annotate-tree/file1:q')
116+ e = self.assertRaises(errors.InvalidRevisionSpec,
117+ spec.as_revision_id, self.tree.branch)
118+ self.assertContainsRe(str(e),
119+ r"Requested revision: \'annotate:annotate-tree/file1:q\' does not"
120+ " exist in branch: .*\nNo such line: q")
121+
122+ def test_no_such_file(self):
123+ spec = RevisionSpec.from_string('annotate:annotate-tree/file2:1')
124+ e = self.assertRaises(errors.InvalidRevisionSpec,
125+ spec.as_revision_id, self.tree.branch)
126+ self.assertContainsRe(str(e),
127+ r"Requested revision: \'annotate:annotate-tree/file2:1\' does not"
128+ " exist in branch: .*\nFile 'file2' is not versioned")
129+
130+ def test_no_such_file_with_colon(self):
131+ spec = RevisionSpec.from_string('annotate:annotate-tree/fi:le2:1')
132+ e = self.assertRaises(errors.InvalidRevisionSpec,
133+ spec.as_revision_id, self.tree.branch)
134+ self.assertContainsRe(str(e),
135+ r"Requested revision: \'annotate:annotate-tree/fi:le2:1\' does not"
136+ " exist in branch: .*\nFile 'fi:le2' is not versioned")