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