Rev 4773: (fullermd) Implement a DWIM revspec type in file:///home/pqm/archives/thelove/bzr/%2Btrunk/

Canonical.com Patch Queue Manager pqm at pqm.ubuntu.com
Tue Oct 27 15:58:03 GMT 2009


At file:///home/pqm/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 4773 [merge]
revision-id: pqm at pqm.ubuntu.com-20091027155759-zjw6vnvs7fyk0lch
parent: pqm at pqm.ubuntu.com-20091027133256-ppg47nq6cvqdycze
parent: v.ladeuil+lp at free.fr-20091027141340-995wzkfrtwbahkrc
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Tue 2009-10-27 15:57:59 +0000
message:
  (fullermd) Implement a DWIM revspec type
modified:
  NEWS                           NEWS-20050323055033-4e00b5db738777ff
  bzrlib/help_topics/__init__.py help_topics.py-20060920210027-rnim90q9e0bwxvy4-1
  bzrlib/option.py               option.py-20051014052914-661fb36e76e7362f
  bzrlib/revisionspec.py         revisionspec.py-20050907152633-17567659fd5c0ddb
  bzrlib/tests/test_revisionspec.py testrevisionnamespaces.py-20050711050225-8b4af89e6b1efe84
  doc/en/user-guide/specifying_revisions.txt specifying_revisions.txt-20060314161707-19deb139101bea33
=== modified file 'NEWS'
--- a/NEWS	2009-10-26 15:59:54 +0000
+++ b/NEWS	2009-10-27 14:13:40 +0000
@@ -254,6 +254,10 @@
 Improvements
 ************
 
+* Revision specifiers can now be given in a more DWIM form, without
+  needing explicit prefixes for specifiers like tags or revision id's.
+  See ``bzr help revisionspec`` for full details.  (Matthew Fuller)
+
 * Bazaar gives a warning before exiting, and writes into ``.bzr.log``, if 
   compiled extensions can't be loaded.  This typically indicates a
   packaging or installation problem.  In this case Bazaar will keep

=== modified file 'bzrlib/help_topics/__init__.py'
--- a/bzrlib/help_topics/__init__.py	2009-10-13 20:15:45 +0000
+++ b/bzrlib/help_topics/__init__.py	2009-10-27 14:13:40 +0000
@@ -152,10 +152,16 @@
     out.append(
 """Revision Identifiers
 
-A revision identifier refers to a specific state of a branch's history. It can
-be a revision number, or a keyword followed by ':' and often other
-parameters. Some examples of identifiers are '3', 'last:1', 'before:yesterday'
-and 'submit:'.
+A revision identifier refers to a specific state of a branch's history.  It
+can be expressed in several ways.  It can begin with a keyword to
+unambiguously specify a given lookup type; some examples are 'last:1',
+'before:yesterday' and 'submit:'.
+
+Alternately, it can be given without a keyword, in which case it will be
+checked as a revision number, a tag, a revision id, a date specification, or a
+branch specification, in that order.  For example, 'date:today' could be
+written as simply 'today', though if you have a tag called 'today' that will
+be found first.
 
 If 'REV1' and 'REV2' are revision identifiers, then 'REV1..REV2' denotes a
 revision range. Examples: '3647..3649', 'date:yesterday..-1' and

=== modified file 'bzrlib/option.py'
--- a/bzrlib/option.py	2009-04-03 20:05:25 +0000
+++ b/bzrlib/option.py	2009-08-18 08:10:44 +0000
@@ -40,25 +40,25 @@
     each revision specifier supplied.
 
     >>> _parse_revision_str('234')
-    [<RevisionSpec_revno 234>]
+    [<RevisionSpec_dwim 234>]
     >>> _parse_revision_str('234..567')
-    [<RevisionSpec_revno 234>, <RevisionSpec_revno 567>]
+    [<RevisionSpec_dwim 234>, <RevisionSpec_dwim 567>]
     >>> _parse_revision_str('..')
     [<RevisionSpec None>, <RevisionSpec None>]
     >>> _parse_revision_str('..234')
-    [<RevisionSpec None>, <RevisionSpec_revno 234>]
+    [<RevisionSpec None>, <RevisionSpec_dwim 234>]
     >>> _parse_revision_str('234..')
-    [<RevisionSpec_revno 234>, <RevisionSpec None>]
+    [<RevisionSpec_dwim 234>, <RevisionSpec None>]
     >>> _parse_revision_str('234..456..789') # Maybe this should be an error
-    [<RevisionSpec_revno 234>, <RevisionSpec_revno 456>, <RevisionSpec_revno 789>]
+    [<RevisionSpec_dwim 234>, <RevisionSpec_dwim 456>, <RevisionSpec_dwim 789>]
     >>> _parse_revision_str('234....789') #Error ?
-    [<RevisionSpec_revno 234>, <RevisionSpec None>, <RevisionSpec_revno 789>]
+    [<RevisionSpec_dwim 234>, <RevisionSpec None>, <RevisionSpec_dwim 789>]
     >>> _parse_revision_str('revid:test at other.com-234234')
     [<RevisionSpec_revid revid:test at other.com-234234>]
     >>> _parse_revision_str('revid:test at other.com-234234..revid:test at other.com-234235')
     [<RevisionSpec_revid revid:test at other.com-234234>, <RevisionSpec_revid revid:test at other.com-234235>]
     >>> _parse_revision_str('revid:test at other.com-234234..23')
-    [<RevisionSpec_revid revid:test at other.com-234234>, <RevisionSpec_revno 23>]
+    [<RevisionSpec_revid revid:test at other.com-234234>, <RevisionSpec_dwim 23>]
     >>> _parse_revision_str('date:2005-04-12')
     [<RevisionSpec_date date:2005-04-12>]
     >>> _parse_revision_str('date:2005-04-12 12:24:33')
@@ -68,27 +68,23 @@
     >>> _parse_revision_str('date:2005-04-12,12:24:33')
     [<RevisionSpec_date date:2005-04-12,12:24:33>]
     >>> _parse_revision_str('-5..23')
-    [<RevisionSpec_revno -5>, <RevisionSpec_revno 23>]
+    [<RevisionSpec_dwim -5>, <RevisionSpec_dwim 23>]
     >>> _parse_revision_str('-5')
-    [<RevisionSpec_revno -5>]
+    [<RevisionSpec_dwim -5>]
     >>> _parse_revision_str('123a')
-    Traceback (most recent call last):
-      ...
-    NoSuchRevisionSpec: No namespace registered for string: '123a'
+    [<RevisionSpec_dwim 123a>]
     >>> _parse_revision_str('abc')
-    Traceback (most recent call last):
-      ...
-    NoSuchRevisionSpec: No namespace registered for string: 'abc'
+    [<RevisionSpec_dwim abc>]
     >>> _parse_revision_str('branch:../branch2')
     [<RevisionSpec_branch branch:../branch2>]
     >>> _parse_revision_str('branch:../../branch2')
     [<RevisionSpec_branch branch:../../branch2>]
     >>> _parse_revision_str('branch:../../branch2..23')
-    [<RevisionSpec_branch branch:../../branch2>, <RevisionSpec_revno 23>]
+    [<RevisionSpec_branch branch:../../branch2>, <RevisionSpec_dwim 23>]
     >>> _parse_revision_str('branch:..\\\\branch2')
     [<RevisionSpec_branch branch:..\\branch2>]
     >>> _parse_revision_str('branch:..\\\\..\\\\branch2..23')
-    [<RevisionSpec_branch branch:..\\..\\branch2>, <RevisionSpec_revno 23>]
+    [<RevisionSpec_branch branch:..\\..\\branch2>, <RevisionSpec_dwim 23>]
     """
     # TODO: Maybe move this into revisionspec.py
     revs = []
@@ -104,7 +100,7 @@
     parent of the revision.
 
     >>> _parse_change_str('123')
-    (<RevisionSpec_before before:123>, <RevisionSpec_revno 123>)
+    (<RevisionSpec_before before:123>, <RevisionSpec_dwim 123>)
     >>> _parse_change_str('123..124')
     Traceback (most recent call last):
       ...

=== modified file 'bzrlib/revisionspec.py'
--- a/bzrlib/revisionspec.py	2009-07-25 08:26:42 +0000
+++ b/bzrlib/revisionspec.py	2009-10-27 14:04:29 +0000
@@ -113,8 +113,6 @@
         return RevisionInfo(branch, revno, revision_id)
 
 
-# classes in this list should have a "prefix" attribute, against which
-# string specs are matched
 _revno_regex = None
 
 
@@ -123,10 +121,10 @@
 
     help_txt = """A parsed revision specification.
 
-    A revision specification can be an integer, in which case it is
-    assumed to be a revno (though this will translate negative values
-    into positive ones); or it can be a string, in which case it is
-    parsed for something like 'date:' or 'revid:' etc.
+    A revision specification is a string, which may be unambiguous about
+    what it represents by giving a prefix like 'date:' or 'revid:' etc,
+    or it may have no prefix, in which case it's tried against several
+    specifier types in sequence to determine what the user meant.
 
     Revision specs are an UI element, and they have been moved out
     of the branch class to leave "back-end" classes unaware of such
@@ -139,6 +137,14 @@
 
     prefix = None
     wants_revision_history = True
+    dwim_catchable_exceptions = (errors.InvalidRevisionSpec,)
+    """Exceptions that RevisionSpec_dwim._match_on will catch.
+
+    If the revspec is part of ``dwim_revspecs``, it may be tried with an
+    invalid revspec and raises some exception. The exceptions mentioned here
+    will not be reported to the user but simply ignored without stopping the
+    dwim processing.
+    """
 
     @staticmethod
     def from_string(spec):
@@ -165,17 +171,9 @@
                     trace.mutter('Returning RevisionSpec %s for %s',
                                  spectype.__name__, spec)
                     return spectype(spec, _internal=True)
-            # RevisionSpec_revno is special cased, because it is the only
-            # one that directly handles plain integers
-            # TODO: This should not be special cased rather it should be
-            # a method invocation on spectype.canparse()
-            global _revno_regex
-            if _revno_regex is None:
-                _revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
-            if _revno_regex.match(spec) is not None:
-                return RevisionSpec_revno(spec, _internal=True)
-
-            raise errors.NoSuchRevisionSpec(spec)
+            # Otherwise treat it as a DWIM, build the RevisionSpec object and
+            # wait for _match_on to be called.
+            return RevisionSpec_dwim(spec, _internal=True)
 
     def __init__(self, spec, _internal=False):
         """Create a RevisionSpec referring to the Null revision.
@@ -290,16 +288,62 @@
 
 # private API
 
+class RevisionSpec_dwim(RevisionSpec):
+    """Provides a DWIMish revision specifier lookup.
+
+    Note that this does not go in the revspec_registry because by definition
+    there is no prefix to identify it.  It's solely called from
+    RevisionSpec.from_string() because the DWIMification happen when _match_on
+    is called so the string describing the revision is kept here until needed.
+    """
+
+    help_txt = None
+    # We don't need to build the revision history ourself, that's delegated to
+    # each revspec we try.
+    wants_revision_history = False
+
+    def _try_spectype(self, rstype, branch):
+        rs = rstype(self.spec, _internal=True)
+        # Hit in_history to find out if it exists, or we need to try the
+        # next type.
+        return rs.in_history(branch)
+
+    def _match_on(self, branch, revs):
+        """Run the lookup and see what we can get."""
+
+        # First, see if it's a revno
+        global _revno_regex
+        if _revno_regex is None:
+            _revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
+        if _revno_regex.match(self.spec) is not None:
+            try:
+                return self._try_spectype(RevisionSpec_revno, branch)
+            except RevisionSpec_revno.dwim_catchable_exceptions:
+                pass
+
+        # Next see what has been registered
+        for rs_class in dwim_revspecs:
+            try:
+                return self._try_spectype(rs_class, branch)
+            except rs_class.dwim_catchable_exceptions:
+                pass
+
+        # Well, I dunno what it is. Note that we don't try to keep track of the
+        # first of last exception raised during the DWIM tries as none seems
+        # really relevant.
+        raise errors.InvalidRevisionSpec(self.spec, branch)
+
+
 class RevisionSpec_revno(RevisionSpec):
     """Selects a revision using a number."""
 
     help_txt = """Selects a revision using a number.
 
     Use an integer to specify a revision in the history of the branch.
-    Optionally a branch can be specified. The 'revno:' prefix is optional.
-    A negative number will count from the end of the branch (-1 is the
-    last revision, -2 the previous one). If the negative number is larger
-    than the branch's history, the first revision is returned.
+    Optionally a branch can be specified.  A negative number will count
+    from the end of the branch (-1 is the last revision, -2 the previous
+    one). If the negative number is larger than the branch's history, the
+    first revision is returned.
     Examples::
 
       revno:1                   -> return the first revision of this branch
@@ -561,6 +605,7 @@
     """
 
     prefix = 'tag:'
+    dwim_catchable_exceptions = (errors.NoSuchTag, errors.TagsNotSupported)
 
     def _match_on(self, branch, revs):
         # Can raise tags not supported, NoSuchTag, etc
@@ -760,6 +805,7 @@
       branch:/path/to/branch
     """
     prefix = 'branch:'
+    dwim_catchable_exceptions = (errors.NotBranchError,)
 
     def _match_on(self, branch, revs):
         from bzrlib.branch import Branch
@@ -838,6 +884,17 @@
             self._get_submit_location(context_branch))
 
 
+# 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
+dwim_revspecs = [
+    RevisionSpec_tag, # Let's try for a tag
+    RevisionSpec_revid, # Maybe it's a revid?
+    RevisionSpec_date, # Perhaps a date?
+    RevisionSpec_branch, # OK, last try, maybe it's a branch
+    ]
+
+
 revspec_registry = registry.Registry()
 def _register_revspec(revspec):
     revspec_registry.register(revspec.prefix, revspec)
@@ -852,5 +909,7 @@
 _register_revspec(RevisionSpec_branch)
 _register_revspec(RevisionSpec_submit)
 
+# classes in this list should have a "prefix" attribute, against which
+# string specs are matched
 SPEC_TYPES = symbol_versioning.deprecated_list(
     symbol_versioning.deprecated_in((1, 12, 0)), "SPEC_TYPES", [])

=== modified file 'bzrlib/tests/test_revisionspec.py'
--- a/bzrlib/tests/test_revisionspec.py	2009-03-23 14:59:43 +0000
+++ b/bzrlib/tests/test_revisionspec.py	2009-10-10 05:07:35 +0000
@@ -146,24 +146,49 @@
     def test_object(self):
         self.assertRaises(TypeError, RevisionSpec.from_string, object())
 
-    def test_unregistered_spec(self):
-        self.assertRaises(errors.NoSuchRevisionSpec,
-                          RevisionSpec.from_string, 'foo')
-        self.assertRaises(errors.NoSuchRevisionSpec,
-                          RevisionSpec.from_string, '123a')
-
-
-
-class TestRevnoFromString(TestCase):
-
-    def test_from_string_dotted_decimal(self):
-        self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '-1.1')
-        self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '.1')
-        self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '1..1')
-        self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '1.2..1')
-        self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '1.')
-        self.assertIsInstance(RevisionSpec.from_string('1.1'), RevisionSpec_revno)
-        self.assertIsInstance(RevisionSpec.from_string('1.1.3'), RevisionSpec_revno)
+
+class TestRevisionSpec_dwim(TestRevisionSpec):
+
+    # Don't need to test revno's explicitly since TRS_revno already
+    # covers that well for us
+    def test_dwim_spec_revno(self):
+        self.assertInHistoryIs(2, 'r2', '2')
+        self.assertAsRevisionId('alt_r2', '1.1.1')
+
+    def test_dwim_spec_revid(self):
+        self.assertInHistoryIs(2, 'r2', 'r2')
+
+    def test_dwim_spec_tag(self):
+        self.tree.branch.tags.set_tag('footag', 'r1')
+        self.assertAsRevisionId('r1', 'footag')
+        self.tree.branch.tags.delete_tag('footag')
+        self.assertRaises(errors.InvalidRevisionSpec,
+                          self.get_in_history, 'footag')
+
+    def test_dwim_spec_tag_that_looks_like_revno(self):
+        # Test that we slip past revno with things that look like revnos,
+        # but aren't.  Tags are convenient for testing this since we can
+        # make them look however we want.
+        self.tree.branch.tags.set_tag('3', 'r2')
+        self.assertAsRevisionId('r2', '3')
+        self.build_tree(['tree/b'])
+        self.tree.add(['b'])
+        self.tree.commit('b', rev_id='r3')
+        self.assertAsRevisionId('r3', '3')
+
+    def test_dwim_spec_date(self):
+        self.assertAsRevisionId('r1', 'today')
+
+    def test_dwim_spec_branch(self):
+        self.assertInHistoryIs(None, 'alt_r2', 'tree2')
+
+    def test_dwim_spec_nonexistent(self):
+        self.assertInvalid('somethingrandom', invalid_as_revision_id=False)
+        self.assertInvalid('-1.1', invalid_as_revision_id=False)
+        self.assertInvalid('.1', invalid_as_revision_id=False)
+        self.assertInvalid('1..1', invalid_as_revision_id=False)
+        self.assertInvalid('1.2..1', invalid_as_revision_id=False)
+        self.assertInvalid('1.', invalid_as_revision_id=False)
 
 
 class TestRevisionSpec_revno(TestRevisionSpec):

=== modified file 'doc/en/user-guide/specifying_revisions.txt'
--- a/doc/en/user-guide/specifying_revisions.txt	2009-04-04 02:57:47 +0000
+++ b/doc/en/user-guide/specifying_revisions.txt	2009-07-25 15:21:25 +0000
@@ -36,17 +36,23 @@
  +----------------------+------------------------------------+
  | *number*             | revision number                    |
  +----------------------+------------------------------------+
- | **revno**:*number*   | positive revision number           |
+ | **revno**:*number*   | revision number                    |
  +----------------------+------------------------------------+
  | **last**:*number*    | negative revision number           |
  +----------------------+------------------------------------+
+ | *guid*               | globally unique revision id        |
+ +----------------------+------------------------------------+
  | **revid**:*guid*     | globally unique revision id        |
  +----------------------+------------------------------------+
  | **before**:*rev*     | leftmost parent of ''rev''         |
  +----------------------+------------------------------------+
- | **date**:*value*     | first entry after a given date     |
- +----------------------+------------------------------------+
- | **tag**:*value*      | revision matching a given tag      |
+ | *date-value*         | first entry after a given date     |
+ +----------------------+------------------------------------+
+ | **date**:*date-value*| first entry after a given date     |
+ +----------------------+------------------------------------+
+ | *tag-name*           | revision matching a given tag      |
+ +----------------------+------------------------------------+
+ | **tag**:*tag-name*   | revision matching a given tag      |
  +----------------------+------------------------------------+
  | **ancestor**:*path*  | last merged revision from a branch |
  +----------------------+------------------------------------+




More information about the bazaar-commits mailing list