Rev 5443: (spiv) Add 'mainline' and 'annotate' revision specs. (Aaron Bentley) (Andrew in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Fri Sep 24 09:18:22 BST 2010
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 5443 [merge]
revision-id: pqm at pqm.ubuntu.com-20100924081819-5b3m10xulgg6d3cv
parent: pqm at pqm.ubuntu.com-20100924065915-djj64ixcdc33s6f4
parent: andrew.bennetts at canonical.com-20100924021953-0cg1kjtifuvkbyzp
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Fri 2010-09-24 09:18:19 +0100
message:
(spiv) Add 'mainline' and 'annotate' revision specs. (Aaron Bentley) (Andrew
Bennetts)
modified:
NEWS NEWS-20050323055033-4e00b5db738777ff
bzrlib/graph.py graph_walker.py-20070525030359-y852guab65d4wtn0-1
bzrlib/repository.py rev_storage.py-20051111201905-119e9401e46257e3
bzrlib/revisionspec.py revisionspec.py-20050907152633-17567659fd5c0ddb
bzrlib/tests/test_graph.py test_graph_walker.py-20070525030405-enq4r60hhi9xrujc-1
bzrlib/tests/test_revisionspec.py testrevisionnamespaces.py-20050711050225-8b4af89e6b1efe84
doc/en/_templates/index.html index.html-20090722133849-lus2rzwsmlhpgqhv-1
doc/en/whats-new/whats-new-in-2.3.txt whatsnewin2.3.txt-20100818072501-x2h25r7jbnknvy30-1
=== modified file 'NEWS'
--- a/NEWS 2010-09-23 07:40:07 +0000
+++ b/NEWS 2010-09-24 02:19:28 +0000
@@ -16,6 +16,12 @@
New Features
************
+* Add ``mainline`` revision specifier, which selects the revision that
+ merged a specified revision into the mainline. (Aaron Bentley)
+
+* Add ``annotate`` revision specifier, which selects the revision that
+ introduced a specified line of a file. (Aaron Bentley)
+
Bug Fixes
*********
=== modified file 'bzrlib/graph.py'
--- a/bzrlib/graph.py 2010-04-10 01:22:00 +0000
+++ b/bzrlib/graph.py 2010-08-15 15:20:14 +0000
@@ -258,6 +258,40 @@
right = searchers[1].seen
return (left.difference(right), right.difference(left))
+ def find_descendants(self, old_key, new_key):
+ """Find descendants of old_key that are ancestors of new_key."""
+ child_map = self.get_child_map(self._find_descendant_ancestors(
+ old_key, new_key))
+ graph = Graph(DictParentsProvider(child_map))
+ searcher = graph._make_breadth_first_searcher([old_key])
+ list(searcher)
+ return searcher.seen
+
+ def _find_descendant_ancestors(self, old_key, new_key):
+ """Find ancestors of new_key that may be descendants of old_key."""
+ stop = self._make_breadth_first_searcher([old_key])
+ descendants = self._make_breadth_first_searcher([new_key])
+ for revisions in descendants:
+ old_stop = stop.seen.intersection(revisions)
+ descendants.stop_searching_any(old_stop)
+ seen_stop = descendants.find_seen_ancestors(stop.step())
+ descendants.stop_searching_any(seen_stop)
+ return descendants.seen.difference(stop.seen)
+
+ def get_child_map(self, keys):
+ """Get a mapping from parents to children of the specified keys.
+
+ This is simply the inversion of get_parent_map. Only supplied keys
+ will be discovered as children.
+ :return: a dict of key:child_list for keys.
+ """
+ parent_map = self._parents_provider.get_parent_map(keys)
+ parent_child = {}
+ for child, parents in sorted(parent_map.items()):
+ for parent in parents:
+ parent_child.setdefault(parent, []).append(child)
+ return parent_child
+
def find_distance_to_null(self, target_revision_id, known_revision_ids):
"""Find the left-hand distance to the NULL_REVISION.
@@ -862,6 +896,26 @@
stop.add(parent_id)
return found
+ def find_lefthand_merger(self, merged_key, tip_key):
+ """Find the first lefthand ancestor of tip_key that merged merged_key.
+
+ We do this by first finding the descendants of merged_key, then
+ walking through the lefthand ancestry of tip_key until we find a key
+ that doesn't descend from merged_key. Its child is the key that
+ merged merged_key.
+
+ :return: The first lefthand ancestor of tip_key to merge merged_key.
+ merged_key if it is a lefthand ancestor of tip_key.
+ None if no ancestor of tip_key merged merged_key.
+ """
+ descendants = self.find_descendants(merged_key, tip_key)
+ candidate_iterator = self.iter_lefthand_ancestry(tip_key)
+ last_candidate = None
+ for candidate in candidate_iterator:
+ if candidate not in descendants:
+ return last_candidate
+ last_candidate = candidate
+
def find_unique_lca(self, left_revision, right_revision,
count_steps=False):
"""Find a unique LCA.
@@ -919,6 +973,25 @@
yield (ghost, None)
pending = next_pending
+ def iter_lefthand_ancestry(self, start_key, stop_keys=None):
+ if stop_keys is None:
+ stop_keys = ()
+ next_key = start_key
+ def get_parents(key):
+ try:
+ return self._parents_provider.get_parent_map([key])[key]
+ except KeyError:
+ raise errors.RevisionNotPresent(next_key, self)
+ while True:
+ if next_key in stop_keys:
+ return
+ parents = get_parents(next_key)
+ yield next_key
+ if len(parents) == 0:
+ return
+ else:
+ next_key = parents[0]
+
def iter_topo_order(self, revisions):
"""Iterate through the input revisions in topological order.
=== modified file 'bzrlib/repository.py'
--- a/bzrlib/repository.py 2010-09-14 02:54:53 +0000
+++ b/bzrlib/repository.py 2010-09-24 01:59:46 +0000
@@ -2511,19 +2511,8 @@
ancestors will be traversed.
"""
graph = self.get_graph()
- next_id = revision_id
- while True:
- if next_id in (None, _mod_revision.NULL_REVISION):
- return
- try:
- parents = graph.get_parent_map([next_id])[next_id]
- except KeyError:
- raise errors.RevisionNotPresent(next_id, self)
- yield next_id
- if len(parents) == 0:
- return
- else:
- next_id = parents[0]
+ stop_revisions = (None, _mod_revision.NULL_REVISION)
+ return graph.iter_lefthand_ancestry(revision_id, stop_revisions)
def is_shared(self):
"""Return True if this repository is flagged as a shared repository."""
=== modified file 'bzrlib/revisionspec.py'
--- a/bzrlib/revisionspec.py 2010-06-24 20:51:59 +0000
+++ b/bzrlib/revisionspec.py 2010-08-18 14:39:37 +0000
@@ -24,12 +24,14 @@
""")
from bzrlib import (
+ branch as _mod_branch,
errors,
osutils,
registry,
revision,
symbol_versioning,
trace,
+ workingtree,
)
@@ -444,7 +446,14 @@
-class RevisionSpec_revid(RevisionSpec):
+class RevisionIDSpec(RevisionSpec):
+
+ def _match_on(self, branch, revs):
+ revision_id = self.as_revision_id(branch)
+ return RevisionInfo.from_revision_id(branch, revision_id, revs)
+
+
+class RevisionSpec_revid(RevisionIDSpec):
"""Selects a revision using the revision id."""
help_txt = """Selects a revision using the revision id.
@@ -459,14 +468,10 @@
prefix = 'revid:'
- def _match_on(self, branch, revs):
+ def _as_revision_id(self, context_branch):
# self.spec comes straight from parsing the command line arguments,
# so we expect it to be a Unicode string. Switch it to the internal
# representation.
- revision_id = osutils.safe_revision_id(self.spec, warn=False)
- return RevisionInfo.from_revision_id(branch, revision_id, revs)
-
- def _as_revision_id(self, context_branch):
return osutils.safe_revision_id(self.spec, warn=False)
@@ -896,6 +901,73 @@
self._get_submit_location(context_branch))
+class RevisionSpec_annotate(RevisionIDSpec):
+
+ prefix = 'annotate:'
+
+ help_txt = """Select the revision that last modified the specified line.
+
+ Select the revision that last modified the specified line. Line is
+ specified as path:number. Path is a relative path to the file. Numbers
+ start at 1, and are relative to the current version, not the last-
+ committed version of the file.
+ """
+
+ def _raise_invalid(self, numstring, context_branch):
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
+ 'No such line: %s' % numstring)
+
+ def _as_revision_id(self, context_branch):
+ path, numstring = self.spec.rsplit(':', 1)
+ try:
+ index = int(numstring) - 1
+ except ValueError:
+ self._raise_invalid(numstring, context_branch)
+ tree, file_path = workingtree.WorkingTree.open_containing(path)
+ tree.lock_read()
+ try:
+ file_id = tree.path2id(file_path)
+ if file_id is None:
+ raise errors.InvalidRevisionSpec(self.user_spec,
+ context_branch, "File '%s' is not versioned." %
+ file_path)
+ revision_ids = [r for (r, l) in tree.annotate_iter(file_id)]
+ finally:
+ tree.unlock()
+ try:
+ revision_id = revision_ids[index]
+ except IndexError:
+ self._raise_invalid(numstring, context_branch)
+ if revision_id == revision.CURRENT_REVISION:
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
+ 'Line %s has not been committed.' % numstring)
+ return revision_id
+
+
+class RevisionSpec_mainline(RevisionIDSpec):
+
+ help_txt = """Select mainline revision that merged the specified revision.
+
+ Select the revision that merged the specified revision into mainline.
+ """
+
+ prefix = 'mainline:'
+
+ def _as_revision_id(self, context_branch):
+ revspec = RevisionSpec.from_string(self.spec)
+ if revspec.get_branch() is None:
+ spec_branch = context_branch
+ else:
+ spec_branch = _mod_branch.Branch.open(revspec.get_branch())
+ revision_id = revspec.as_revision_id(spec_branch)
+ graph = context_branch.repository.get_graph()
+ result = graph.find_lefthand_merger(revision_id,
+ context_branch.last_revision())
+ if result is None:
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
+ return result
+
+
# The order in which we want to DWIM a revision spec without any prefix.
# revno is always tried first and isn't listed here, this is used by
# RevisionSpec_dwim._match_on
@@ -920,6 +992,8 @@
_register_revspec(RevisionSpec_ancestor)
_register_revspec(RevisionSpec_branch)
_register_revspec(RevisionSpec_submit)
+_register_revspec(RevisionSpec_annotate)
+_register_revspec(RevisionSpec_mainline)
# classes in this list should have a "prefix" attribute, against which
# string specs are matched
=== modified file 'bzrlib/tests/test_graph.py'
--- a/bzrlib/tests/test_graph.py 2010-02-17 17:11:16 +0000
+++ b/bzrlib/tests/test_graph.py 2010-08-15 15:20:14 +0000
@@ -1424,6 +1424,61 @@
self.assertMergeOrder(['rev3', 'rev1'], graph, 'rev4', ['rev1', 'rev3'])
+class TestFindDescendants(TestGraphBase):
+
+ def test_find_descendants_rev1_rev3(self):
+ graph = self.make_graph(ancestry_1)
+ descendants = graph.find_descendants('rev1', 'rev3')
+ self.assertEqual(set(['rev1', 'rev2a', 'rev3']), descendants)
+
+ def test_find_descendants_rev1_rev4(self):
+ graph = self.make_graph(ancestry_1)
+ descendants = graph.find_descendants('rev1', 'rev4')
+ self.assertEqual(set(['rev1', 'rev2a', 'rev2b', 'rev3', 'rev4']),
+ descendants)
+
+ def test_find_descendants_rev2a_rev4(self):
+ graph = self.make_graph(ancestry_1)
+ descendants = graph.find_descendants('rev2a', 'rev4')
+ self.assertEqual(set(['rev2a', 'rev3', 'rev4']), descendants)
+
+class TestFindLefthandMerger(TestGraphBase):
+
+ def check_merger(self, result, ancestry, merged, tip):
+ graph = self.make_graph(ancestry)
+ self.assertEqual(result, graph.find_lefthand_merger(merged, tip))
+
+ def test_find_lefthand_merger_rev2b(self):
+ self.check_merger('rev4', ancestry_1, 'rev2b', 'rev4')
+
+ def test_find_lefthand_merger_rev2a(self):
+ self.check_merger('rev2a', ancestry_1, 'rev2a', 'rev4')
+
+ def test_find_lefthand_merger_rev4(self):
+ self.check_merger(None, ancestry_1, 'rev4', 'rev2a')
+
+ def test_find_lefthand_merger_f(self):
+ self.check_merger('i', complex_shortcut, 'f', 'm')
+
+ def test_find_lefthand_merger_g(self):
+ self.check_merger('i', complex_shortcut, 'g', 'm')
+
+ def test_find_lefthand_merger_h(self):
+ self.check_merger('n', complex_shortcut, 'h', 'n')
+
+
+class TestGetChildMap(TestGraphBase):
+
+ def test_get_child_map(self):
+ graph = self.make_graph(ancestry_1)
+ child_map = graph.get_child_map(['rev4', 'rev3', 'rev2a', 'rev2b'])
+ self.assertEqual({'rev1': ['rev2a', 'rev2b'],
+ 'rev2a': ['rev3'],
+ 'rev2b': ['rev4'],
+ 'rev3': ['rev4']},
+ child_map)
+
+
class TestCachingParentsProvider(tests.TestCase):
"""These tests run with:
=== modified file 'bzrlib/tests/test_revisionspec.py'
--- a/bzrlib/tests/test_revisionspec.py 2010-02-17 17:11:16 +0000
+++ b/bzrlib/tests/test_revisionspec.py 2010-09-24 02:19:53 +0000
@@ -652,3 +652,79 @@
def test_as_revision_id(self):
self.tree.branch.set_submit_branch('tree2')
self.assertAsRevisionId('alt_r2', 'branch:tree2')
+
+
+class TestRevisionSpec_mainline(TestRevisionSpec):
+
+ def test_as_revision_id(self):
+ self.assertAsRevisionId('r1', 'mainline:1')
+ self.assertAsRevisionId('r2', 'mainline:1.1.1')
+ self.assertAsRevisionId('r2', 'mainline:revid:alt_r2')
+ spec = RevisionSpec.from_string('mainline:revid:alt_r22')
+ e = self.assertRaises(errors.InvalidRevisionSpec,
+ spec.as_revision_id, self.tree.branch)
+ self.assertContainsRe(str(e),
+ "Requested revision: 'mainline:revid:alt_r22' does not exist in"
+ " branch: ")
+
+ def test_in_history(self):
+ self.assertInHistoryIs(2, 'r2', 'mainline:revid:alt_r2')
+
+
+class TestRevisionSpec_annotate(TestRevisionSpec):
+
+ def setUp(self):
+ TestRevisionSpec.setUp(self)
+ self.tree = self.make_branch_and_tree('annotate-tree')
+ self.build_tree_contents([('annotate-tree/file1', '1\n')])
+ self.tree.add('file1')
+ self.tree.commit('r1', rev_id='r1')
+ self.build_tree_contents([('annotate-tree/file1', '2\n1\n')])
+ self.tree.commit('r2', rev_id='r2')
+ self.build_tree_contents([('annotate-tree/file1', '2\n1\n3\n')])
+
+ def test_as_revision_id_r1(self):
+ self.assertAsRevisionId('r1', 'annotate:annotate-tree/file1:2')
+
+ def test_as_revision_id_r2(self):
+ self.assertAsRevisionId('r2', 'annotate:annotate-tree/file1:1')
+
+ def test_as_revision_id_uncommitted(self):
+ spec = RevisionSpec.from_string('annotate:annotate-tree/file1:3')
+ e = self.assertRaises(errors.InvalidRevisionSpec,
+ spec.as_revision_id, self.tree.branch)
+ self.assertContainsRe(str(e),
+ r"Requested revision: \'annotate:annotate-tree/file1:3\' does not"
+ " exist in branch: .*\nLine 3 has not been committed.")
+
+ def test_non_existent_line(self):
+ spec = RevisionSpec.from_string('annotate:annotate-tree/file1:4')
+ e = self.assertRaises(errors.InvalidRevisionSpec,
+ spec.as_revision_id, self.tree.branch)
+ self.assertContainsRe(str(e),
+ r"Requested revision: \'annotate:annotate-tree/file1:4\' does not"
+ " exist in branch: .*\nNo such line: 4")
+
+ def test_invalid_line(self):
+ spec = RevisionSpec.from_string('annotate:annotate-tree/file1:q')
+ e = self.assertRaises(errors.InvalidRevisionSpec,
+ spec.as_revision_id, self.tree.branch)
+ self.assertContainsRe(str(e),
+ r"Requested revision: \'annotate:annotate-tree/file1:q\' does not"
+ " exist in branch: .*\nNo such line: q")
+
+ def test_no_such_file(self):
+ spec = RevisionSpec.from_string('annotate:annotate-tree/file2:1')
+ e = self.assertRaises(errors.InvalidRevisionSpec,
+ spec.as_revision_id, self.tree.branch)
+ self.assertContainsRe(str(e),
+ r"Requested revision: \'annotate:annotate-tree/file2:1\' does not"
+ " exist in branch: .*\nFile 'file2' is not versioned")
+
+ def test_no_such_file_with_colon(self):
+ spec = RevisionSpec.from_string('annotate:annotate-tree/fi:le2:1')
+ e = self.assertRaises(errors.InvalidRevisionSpec,
+ spec.as_revision_id, self.tree.branch)
+ self.assertContainsRe(str(e),
+ r"Requested revision: \'annotate:annotate-tree/fi:le2:1\' does not"
+ " exist in branch: .*\nFile 'fi:le2' is not versioned")
=== modified file 'doc/en/_templates/index.html'
--- a/doc/en/_templates/index.html 2010-08-13 19:08:57 +0000
+++ b/doc/en/_templates/index.html 2010-09-24 02:19:28 +0000
@@ -7,7 +7,7 @@
<table class="contentstable" align="center" style="margin-left: 30px"><tr>
<td width="50%">
- <p class="biglink"><a class="biglink" href="{{ pathto("whats-new/whats-new-in-2.2") }}">What's New in Bazaar 2.2?</a><br/>
+ <p class="biglink"><a class="biglink" href="{{ pathto("whats-new/whats-new-in-2.3") }}">What's New in Bazaar 2.3?</a><br/>
<span class="linkdescr">what's new in this release</span>
</p>
<p class="biglink"><a class="biglink" href="{{ pathto("user-guide/index") }}">User Guide</a><br/>
=== modified file 'doc/en/whats-new/whats-new-in-2.3.txt'
--- a/doc/en/whats-new/whats-new-in-2.3.txt 2010-09-08 08:49:06 +0000
+++ b/doc/en/whats-new/whats-new-in-2.3.txt 2010-09-24 02:19:28 +0000
@@ -60,8 +60,33 @@
content faster than seeking and reading content from another tree,
especially in cold-cache situations. (John Arbash Meinel, #607298)
+New revision specifiers
+***********************
+
+* The ``mainline`` revision specifier has been added. It takes another revision
+ spec as its input, and selects the revision which merged that revision into
+ the mainline.
+
+ For example, ``bzr log -vp -r mainline:1.2.3`` will show the log of the
+ revision that merged revision 1.2.3 into mainline, along with its status
+ output and diff. (Aaron Bentley)
+
+* The ``annotate`` revision specifier has been added. It takes a path and a
+ line as its input (in the form ``path:line``), and selects the revision which
+ introduced that line of that file.
+
+ 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``
+ (Aaron Bentley)
+
Documentation
*************
+
* A beta version of the documentation is now available in GNU TexInfo
format, used by emacs and the standalone ``info`` reader.
(Vincent Ladeuil, #219334)
More information about the bazaar-commits
mailing list