Merge lp:~spiv/bzr/annotate-revspec into lp:bzr
- annotate-revspec
- Merge into bzr.dev
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 |
Related bugs: |
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:/
To post a comment you must log in.
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 '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) |
sent to pqm by email