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

Proposed by Andrew Bennetts
Status: Merged
Approved by: Andrew Bennetts
Approved revision: no longer in the source branch.
Merged at revision: 5443
Proposed branch: lp:~spiv/bzr/annotate-revspec
Merge into: lp:bzr
Diff against target: 475 lines (+318/-20)
8 files modified
NEWS (+6/-0)
bzrlib/graph.py (+73/-0)
bzrlib/repository.py (+2/-13)
bzrlib/revisionspec.py (+80/-6)
bzrlib/tests/test_graph.py (+55/-0)
bzrlib/tests/test_revisionspec.py (+76/-0)
doc/en/_templates/index.html (+1/-1)
doc/en/whats-new/whats-new-in-2.3.txt (+25/-0)
To merge this branch: bzr merge lp:~spiv/bzr/annotate-revspec
Reviewer Review Type Date Requested Status
bzr-core Pending
Review via email: mp+36524@code.launchpad.net

Commit message

Add 'mainline' and 'annotate' revision specs. (Aaron Bentley)

Description of the change

This is just Aaron's <https://code.edge.launchpad.net/~abentley/bzr/mainline-revspec/+merge/34680> and <https://code.edge.launchpad.net/~abentley/bzr/annotate-revspec/+merge/34681> with the review tweaks applied (Add NEWS & What's New entries, and fix a spelling nit in a test method name).

To post a comment you must log in.
Revision history for this message
Andrew Bennetts (spiv) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2010-09-23 07:40:07 +0000
3+++ NEWS 2010-09-24 02:23:42 +0000
4@@ -16,6 +16,12 @@
5 New Features
6 ************
7
8+* Add ``mainline`` revision specifier, which selects the revision that
9+ merged a specified revision into the mainline. (Aaron Bentley)
10+
11+* Add ``annotate`` revision specifier, which selects the revision that
12+ introduced a specified line of a file. (Aaron Bentley)
13+
14 Bug Fixes
15 *********
16
17
18=== modified file 'bzrlib/graph.py'
19--- bzrlib/graph.py 2010-04-10 01:22:00 +0000
20+++ bzrlib/graph.py 2010-09-24 02:23:42 +0000
21@@ -258,6 +258,40 @@
22 right = searchers[1].seen
23 return (left.difference(right), right.difference(left))
24
25+ def find_descendants(self, old_key, new_key):
26+ """Find descendants of old_key that are ancestors of new_key."""
27+ child_map = self.get_child_map(self._find_descendant_ancestors(
28+ old_key, new_key))
29+ graph = Graph(DictParentsProvider(child_map))
30+ searcher = graph._make_breadth_first_searcher([old_key])
31+ list(searcher)
32+ return searcher.seen
33+
34+ def _find_descendant_ancestors(self, old_key, new_key):
35+ """Find ancestors of new_key that may be descendants of old_key."""
36+ stop = self._make_breadth_first_searcher([old_key])
37+ descendants = self._make_breadth_first_searcher([new_key])
38+ for revisions in descendants:
39+ old_stop = stop.seen.intersection(revisions)
40+ descendants.stop_searching_any(old_stop)
41+ seen_stop = descendants.find_seen_ancestors(stop.step())
42+ descendants.stop_searching_any(seen_stop)
43+ return descendants.seen.difference(stop.seen)
44+
45+ def get_child_map(self, keys):
46+ """Get a mapping from parents to children of the specified keys.
47+
48+ This is simply the inversion of get_parent_map. Only supplied keys
49+ will be discovered as children.
50+ :return: a dict of key:child_list for keys.
51+ """
52+ parent_map = self._parents_provider.get_parent_map(keys)
53+ parent_child = {}
54+ for child, parents in sorted(parent_map.items()):
55+ for parent in parents:
56+ parent_child.setdefault(parent, []).append(child)
57+ return parent_child
58+
59 def find_distance_to_null(self, target_revision_id, known_revision_ids):
60 """Find the left-hand distance to the NULL_REVISION.
61
62@@ -862,6 +896,26 @@
63 stop.add(parent_id)
64 return found
65
66+ def find_lefthand_merger(self, merged_key, tip_key):
67+ """Find the first lefthand ancestor of tip_key that merged merged_key.
68+
69+ We do this by first finding the descendants of merged_key, then
70+ walking through the lefthand ancestry of tip_key until we find a key
71+ that doesn't descend from merged_key. Its child is the key that
72+ merged merged_key.
73+
74+ :return: The first lefthand ancestor of tip_key to merge merged_key.
75+ merged_key if it is a lefthand ancestor of tip_key.
76+ None if no ancestor of tip_key merged merged_key.
77+ """
78+ descendants = self.find_descendants(merged_key, tip_key)
79+ candidate_iterator = self.iter_lefthand_ancestry(tip_key)
80+ last_candidate = None
81+ for candidate in candidate_iterator:
82+ if candidate not in descendants:
83+ return last_candidate
84+ last_candidate = candidate
85+
86 def find_unique_lca(self, left_revision, right_revision,
87 count_steps=False):
88 """Find a unique LCA.
89@@ -919,6 +973,25 @@
90 yield (ghost, None)
91 pending = next_pending
92
93+ def iter_lefthand_ancestry(self, start_key, stop_keys=None):
94+ if stop_keys is None:
95+ stop_keys = ()
96+ next_key = start_key
97+ def get_parents(key):
98+ try:
99+ return self._parents_provider.get_parent_map([key])[key]
100+ except KeyError:
101+ raise errors.RevisionNotPresent(next_key, self)
102+ while True:
103+ if next_key in stop_keys:
104+ return
105+ parents = get_parents(next_key)
106+ yield next_key
107+ if len(parents) == 0:
108+ return
109+ else:
110+ next_key = parents[0]
111+
112 def iter_topo_order(self, revisions):
113 """Iterate through the input revisions in topological order.
114
115
116=== modified file 'bzrlib/repository.py'
117--- bzrlib/repository.py 2010-09-14 02:54:53 +0000
118+++ bzrlib/repository.py 2010-09-24 02:23:42 +0000
119@@ -2511,19 +2511,8 @@
120 ancestors will be traversed.
121 """
122 graph = self.get_graph()
123- next_id = revision_id
124- while True:
125- if next_id in (None, _mod_revision.NULL_REVISION):
126- return
127- try:
128- parents = graph.get_parent_map([next_id])[next_id]
129- except KeyError:
130- raise errors.RevisionNotPresent(next_id, self)
131- yield next_id
132- if len(parents) == 0:
133- return
134- else:
135- next_id = parents[0]
136+ stop_revisions = (None, _mod_revision.NULL_REVISION)
137+ return graph.iter_lefthand_ancestry(revision_id, stop_revisions)
138
139 def is_shared(self):
140 """Return True if this repository is flagged as a shared repository."""
141
142=== modified file 'bzrlib/revisionspec.py'
143--- bzrlib/revisionspec.py 2010-06-24 20:51:59 +0000
144+++ bzrlib/revisionspec.py 2010-09-24 02:23:42 +0000
145@@ -24,12 +24,14 @@
146 """)
147
148 from bzrlib import (
149+ branch as _mod_branch,
150 errors,
151 osutils,
152 registry,
153 revision,
154 symbol_versioning,
155 trace,
156+ workingtree,
157 )
158
159
160@@ -444,7 +446,14 @@
161
162
163
164-class RevisionSpec_revid(RevisionSpec):
165+class RevisionIDSpec(RevisionSpec):
166+
167+ def _match_on(self, branch, revs):
168+ revision_id = self.as_revision_id(branch)
169+ return RevisionInfo.from_revision_id(branch, revision_id, revs)
170+
171+
172+class RevisionSpec_revid(RevisionIDSpec):
173 """Selects a revision using the revision id."""
174
175 help_txt = """Selects a revision using the revision id.
176@@ -459,14 +468,10 @@
177
178 prefix = 'revid:'
179
180- def _match_on(self, branch, revs):
181+ def _as_revision_id(self, context_branch):
182 # self.spec comes straight from parsing the command line arguments,
183 # so we expect it to be a Unicode string. Switch it to the internal
184 # representation.
185- revision_id = osutils.safe_revision_id(self.spec, warn=False)
186- return RevisionInfo.from_revision_id(branch, revision_id, revs)
187-
188- def _as_revision_id(self, context_branch):
189 return osutils.safe_revision_id(self.spec, warn=False)
190
191
192@@ -896,6 +901,73 @@
193 self._get_submit_location(context_branch))
194
195
196+class RevisionSpec_annotate(RevisionIDSpec):
197+
198+ prefix = 'annotate:'
199+
200+ help_txt = """Select the revision that last modified the specified line.
201+
202+ Select the revision that last modified the specified line. Line is
203+ specified as path:number. Path is a relative path to the file. Numbers
204+ start at 1, and are relative to the current version, not the last-
205+ committed version of the file.
206+ """
207+
208+ def _raise_invalid(self, numstring, context_branch):
209+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
210+ 'No such line: %s' % numstring)
211+
212+ def _as_revision_id(self, context_branch):
213+ path, numstring = self.spec.rsplit(':', 1)
214+ try:
215+ index = int(numstring) - 1
216+ except ValueError:
217+ self._raise_invalid(numstring, context_branch)
218+ tree, file_path = workingtree.WorkingTree.open_containing(path)
219+ tree.lock_read()
220+ try:
221+ file_id = tree.path2id(file_path)
222+ if file_id is None:
223+ raise errors.InvalidRevisionSpec(self.user_spec,
224+ context_branch, "File '%s' is not versioned." %
225+ file_path)
226+ revision_ids = [r for (r, l) in tree.annotate_iter(file_id)]
227+ finally:
228+ tree.unlock()
229+ try:
230+ revision_id = revision_ids[index]
231+ except IndexError:
232+ self._raise_invalid(numstring, context_branch)
233+ if revision_id == revision.CURRENT_REVISION:
234+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
235+ 'Line %s has not been committed.' % numstring)
236+ return revision_id
237+
238+
239+class RevisionSpec_mainline(RevisionIDSpec):
240+
241+ help_txt = """Select mainline revision that merged the specified revision.
242+
243+ Select the revision that merged the specified revision into mainline.
244+ """
245+
246+ prefix = 'mainline:'
247+
248+ def _as_revision_id(self, context_branch):
249+ revspec = RevisionSpec.from_string(self.spec)
250+ if revspec.get_branch() is None:
251+ spec_branch = context_branch
252+ else:
253+ spec_branch = _mod_branch.Branch.open(revspec.get_branch())
254+ revision_id = revspec.as_revision_id(spec_branch)
255+ graph = context_branch.repository.get_graph()
256+ result = graph.find_lefthand_merger(revision_id,
257+ context_branch.last_revision())
258+ if result is None:
259+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
260+ return result
261+
262+
263 # The order in which we want to DWIM a revision spec without any prefix.
264 # revno is always tried first and isn't listed here, this is used by
265 # RevisionSpec_dwim._match_on
266@@ -920,6 +992,8 @@
267 _register_revspec(RevisionSpec_ancestor)
268 _register_revspec(RevisionSpec_branch)
269 _register_revspec(RevisionSpec_submit)
270+_register_revspec(RevisionSpec_annotate)
271+_register_revspec(RevisionSpec_mainline)
272
273 # classes in this list should have a "prefix" attribute, against which
274 # string specs are matched
275
276=== modified file 'bzrlib/tests/test_graph.py'
277--- bzrlib/tests/test_graph.py 2010-02-17 17:11:16 +0000
278+++ bzrlib/tests/test_graph.py 2010-09-24 02:23:42 +0000
279@@ -1424,6 +1424,61 @@
280 self.assertMergeOrder(['rev3', 'rev1'], graph, 'rev4', ['rev1', 'rev3'])
281
282
283+class TestFindDescendants(TestGraphBase):
284+
285+ def test_find_descendants_rev1_rev3(self):
286+ graph = self.make_graph(ancestry_1)
287+ descendants = graph.find_descendants('rev1', 'rev3')
288+ self.assertEqual(set(['rev1', 'rev2a', 'rev3']), descendants)
289+
290+ def test_find_descendants_rev1_rev4(self):
291+ graph = self.make_graph(ancestry_1)
292+ descendants = graph.find_descendants('rev1', 'rev4')
293+ self.assertEqual(set(['rev1', 'rev2a', 'rev2b', 'rev3', 'rev4']),
294+ descendants)
295+
296+ def test_find_descendants_rev2a_rev4(self):
297+ graph = self.make_graph(ancestry_1)
298+ descendants = graph.find_descendants('rev2a', 'rev4')
299+ self.assertEqual(set(['rev2a', 'rev3', 'rev4']), descendants)
300+
301+class TestFindLefthandMerger(TestGraphBase):
302+
303+ def check_merger(self, result, ancestry, merged, tip):
304+ graph = self.make_graph(ancestry)
305+ self.assertEqual(result, graph.find_lefthand_merger(merged, tip))
306+
307+ def test_find_lefthand_merger_rev2b(self):
308+ self.check_merger('rev4', ancestry_1, 'rev2b', 'rev4')
309+
310+ def test_find_lefthand_merger_rev2a(self):
311+ self.check_merger('rev2a', ancestry_1, 'rev2a', 'rev4')
312+
313+ def test_find_lefthand_merger_rev4(self):
314+ self.check_merger(None, ancestry_1, 'rev4', 'rev2a')
315+
316+ def test_find_lefthand_merger_f(self):
317+ self.check_merger('i', complex_shortcut, 'f', 'm')
318+
319+ def test_find_lefthand_merger_g(self):
320+ self.check_merger('i', complex_shortcut, 'g', 'm')
321+
322+ def test_find_lefthand_merger_h(self):
323+ self.check_merger('n', complex_shortcut, 'h', 'n')
324+
325+
326+class TestGetChildMap(TestGraphBase):
327+
328+ def test_get_child_map(self):
329+ graph = self.make_graph(ancestry_1)
330+ child_map = graph.get_child_map(['rev4', 'rev3', 'rev2a', 'rev2b'])
331+ self.assertEqual({'rev1': ['rev2a', 'rev2b'],
332+ 'rev2a': ['rev3'],
333+ 'rev2b': ['rev4'],
334+ 'rev3': ['rev4']},
335+ child_map)
336+
337+
338 class TestCachingParentsProvider(tests.TestCase):
339 """These tests run with:
340
341
342=== modified file 'bzrlib/tests/test_revisionspec.py'
343--- bzrlib/tests/test_revisionspec.py 2010-02-17 17:11:16 +0000
344+++ bzrlib/tests/test_revisionspec.py 2010-09-24 02:23:42 +0000
345@@ -652,3 +652,79 @@
346 def test_as_revision_id(self):
347 self.tree.branch.set_submit_branch('tree2')
348 self.assertAsRevisionId('alt_r2', 'branch:tree2')
349+
350+
351+class TestRevisionSpec_mainline(TestRevisionSpec):
352+
353+ def test_as_revision_id(self):
354+ self.assertAsRevisionId('r1', 'mainline:1')
355+ self.assertAsRevisionId('r2', 'mainline:1.1.1')
356+ self.assertAsRevisionId('r2', 'mainline:revid:alt_r2')
357+ spec = RevisionSpec.from_string('mainline:revid:alt_r22')
358+ e = self.assertRaises(errors.InvalidRevisionSpec,
359+ spec.as_revision_id, self.tree.branch)
360+ self.assertContainsRe(str(e),
361+ "Requested revision: 'mainline:revid:alt_r22' does not exist in"
362+ " branch: ")
363+
364+ def test_in_history(self):
365+ self.assertInHistoryIs(2, 'r2', 'mainline:revid:alt_r2')
366+
367+
368+class TestRevisionSpec_annotate(TestRevisionSpec):
369+
370+ def setUp(self):
371+ TestRevisionSpec.setUp(self)
372+ self.tree = self.make_branch_and_tree('annotate-tree')
373+ self.build_tree_contents([('annotate-tree/file1', '1\n')])
374+ self.tree.add('file1')
375+ self.tree.commit('r1', rev_id='r1')
376+ self.build_tree_contents([('annotate-tree/file1', '2\n1\n')])
377+ self.tree.commit('r2', rev_id='r2')
378+ self.build_tree_contents([('annotate-tree/file1', '2\n1\n3\n')])
379+
380+ def test_as_revision_id_r1(self):
381+ self.assertAsRevisionId('r1', 'annotate:annotate-tree/file1:2')
382+
383+ def test_as_revision_id_r2(self):
384+ self.assertAsRevisionId('r2', 'annotate:annotate-tree/file1:1')
385+
386+ def test_as_revision_id_uncommitted(self):
387+ spec = RevisionSpec.from_string('annotate:annotate-tree/file1:3')
388+ e = self.assertRaises(errors.InvalidRevisionSpec,
389+ spec.as_revision_id, self.tree.branch)
390+ self.assertContainsRe(str(e),
391+ r"Requested revision: \'annotate:annotate-tree/file1:3\' does not"
392+ " exist in branch: .*\nLine 3 has not been committed.")
393+
394+ def test_non_existent_line(self):
395+ spec = RevisionSpec.from_string('annotate:annotate-tree/file1:4')
396+ e = self.assertRaises(errors.InvalidRevisionSpec,
397+ spec.as_revision_id, self.tree.branch)
398+ self.assertContainsRe(str(e),
399+ r"Requested revision: \'annotate:annotate-tree/file1:4\' does not"
400+ " exist in branch: .*\nNo such line: 4")
401+
402+ def test_invalid_line(self):
403+ spec = RevisionSpec.from_string('annotate:annotate-tree/file1:q')
404+ e = self.assertRaises(errors.InvalidRevisionSpec,
405+ spec.as_revision_id, self.tree.branch)
406+ self.assertContainsRe(str(e),
407+ r"Requested revision: \'annotate:annotate-tree/file1:q\' does not"
408+ " exist in branch: .*\nNo such line: q")
409+
410+ def test_no_such_file(self):
411+ spec = RevisionSpec.from_string('annotate:annotate-tree/file2:1')
412+ e = self.assertRaises(errors.InvalidRevisionSpec,
413+ spec.as_revision_id, self.tree.branch)
414+ self.assertContainsRe(str(e),
415+ r"Requested revision: \'annotate:annotate-tree/file2:1\' does not"
416+ " exist in branch: .*\nFile 'file2' is not versioned")
417+
418+ def test_no_such_file_with_colon(self):
419+ spec = RevisionSpec.from_string('annotate:annotate-tree/fi:le2:1')
420+ e = self.assertRaises(errors.InvalidRevisionSpec,
421+ spec.as_revision_id, self.tree.branch)
422+ self.assertContainsRe(str(e),
423+ r"Requested revision: \'annotate:annotate-tree/fi:le2:1\' does not"
424+ " exist in branch: .*\nFile 'fi:le2' is not versioned")
425
426=== modified file 'doc/en/_templates/index.html'
427--- doc/en/_templates/index.html 2010-08-13 19:08:57 +0000
428+++ doc/en/_templates/index.html 2010-09-24 02:23:42 +0000
429@@ -7,7 +7,7 @@
430
431 <table class="contentstable" align="center" style="margin-left: 30px"><tr>
432 <td width="50%">
433- <p class="biglink"><a class="biglink" href="{{ pathto("whats-new/whats-new-in-2.2") }}">What's New in Bazaar 2.2?</a><br/>
434+ <p class="biglink"><a class="biglink" href="{{ pathto("whats-new/whats-new-in-2.3") }}">What's New in Bazaar 2.3?</a><br/>
435 <span class="linkdescr">what's new in this release</span>
436 </p>
437 <p class="biglink"><a class="biglink" href="{{ pathto("user-guide/index") }}">User Guide</a><br/>
438
439=== modified file 'doc/en/whats-new/whats-new-in-2.3.txt'
440--- doc/en/whats-new/whats-new-in-2.3.txt 2010-09-08 08:49:06 +0000
441+++ doc/en/whats-new/whats-new-in-2.3.txt 2010-09-24 02:23:42 +0000
442@@ -60,8 +60,33 @@
443 content faster than seeking and reading content from another tree,
444 especially in cold-cache situations. (John Arbash Meinel, #607298)
445
446+New revision specifiers
447+***********************
448+
449+* The ``mainline`` revision specifier has been added. It takes another revision
450+ spec as its input, and selects the revision which merged that revision into
451+ the mainline.
452+
453+ For example, ``bzr log -vp -r mainline:1.2.3`` will show the log of the
454+ revision that merged revision 1.2.3 into mainline, along with its status
455+ output and diff. (Aaron Bentley)
456+
457+* The ``annotate`` revision specifier has been added. It takes a path and a
458+ line as its input (in the form ``path:line``), and selects the revision which
459+ introduced that line of that file.
460+
461+ For example: ``bzr log -vp -r annotate:bzrlib/transform.py:500`` will select
462+ the revision that introduced line 500 of transform.py, and display its log,
463+ status output and diff.
464+
465+ It can be combined with ``mainline`` to select the revision that landed this
466+ line into trunk, like so:
467+ ``bzr log -vp -r mainline:annotate:bzrlib/transform.py:500``
468+ (Aaron Bentley)
469+
470 Documentation
471 *************
472+
473 * A beta version of the documentation is now available in GNU TexInfo
474 format, used by emacs and the standalone ``info`` reader.
475 (Vincent Ladeuil, #219334)