Merge lp:~fullermd/bzr/revspec-dwim into lp:bzr

Proposed by Matthew Fuller
Status: Superseded
Proposed branch: lp:~fullermd/bzr/revspec-dwim
Merge into: lp:bzr
Diff against target: 396 lines
6 files modified
NEWS (+4/-0)
bzrlib/help_topics/__init__.py (+10/-4)
bzrlib/option.py (+14/-18)
bzrlib/revisionspec.py (+80/-21)
bzrlib/tests/test_revisionspec.py (+43/-18)
doc/en/user-guide/specifying_revisions.txt (+10/-4)
To merge this branch: bzr merge lp:~fullermd/bzr/revspec-dwim
Reviewer Review Type Date Requested Status
Robert Collins (community) Needs Fixing
Ian Clatworthy Approve
Review via email: mp+13164@code.launchpad.net

This proposal has been superseded by a proposal from 2009-10-22.

To post a comment you must log in.
Revision history for this message
Matthew Fuller (fullermd) wrote :

This allows unqualified revspecs to be tried as revnos, tags, revids, dates, or branches (respectively) rather than being assumed to be revnos.

Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

I agree in principle with this and the code looks ok to me. There are 2 main concerns I have:

1. If something looks like a revno and isn't, I'd prefer that we raised an error in that case rather than continuing to look. The main issue here is tags like x.y.z I guess. I'd prefer we err'ed on the side of safety. It's not a big deal for projects to use a more explicit tag (like release-x.y.z) or for the user to type tag:x.y.z if they choose not to.

2. The time to look up a revision-id needs discussion. I'd prefer if it was done last. (If that's a performance issue, I'd prefer it was done immediately after revno but before the other specifiers.)

Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

After discussing this on IRC with a few people, I'm ok to proceed with this as is. Matthew has pinged the list just in case others have strong opinions. We ought to give that a day or two to see what feedback, if any, there is.

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

 review needs-fixing

(however thats said now :()

This manually reproduces the registry of revspecs. Lets not do that,
instead query it dynamically.

If an ordering is needed, see the transport registry which has some
sorting done to it.

-Rob

review: Needs Fixing
Revision history for this message
Matthew Fuller (fullermd) wrote :

On Thu, Oct 15, 2009 at 05:33:12AM -0000 I heard the voice of
Robert Collins, and lo! it spake thus:
>
> This manually reproduces the registry of revspecs. Lets not do that,
> instead query it dynamically.
>
> If an ordering is needed, see the transport registry which has some
> sorting done to it.

I disagree.

Leaving aside the very large practical difficulties in doing it out of
the registry (and there are several), I consider it the wrong
approach. This isn't RevisionSpec_tryeverything, it's
RevisionSpec_dwim. An important part of a non-screwyou DWIM system is
carefully limiting to scope, and precisely defining the order within
that.

In this case, the dwim tries just half (rounded) of the revspecs we
have, because trying the rest makes no sense at all (e.g., if we don't
match it somewhere else, trying to match it to 'before:' would be
nonsensical, and in the bizarre case it actually DID something, it's
sure to be Wrong).

--
Matthew Fuller
<email address hidden>

Revision history for this message
Robert Collins (lifeless) wrote :

I disagree with your disagreement :)

specifically, git:, hg:, svn:, and thread: specs are added by plugins
and desirable to search too.

You can add this as a future enchancement if you like, but I think the
code will be smaller and more compact if you go straight to this.

-Rob

Revision history for this message
Matthew Fuller (fullermd) wrote :

> specifically, git:, hg:, svn:, and thread: specs are added by
> plugins and desirable to search too.

OK, so it sounds like there're actually two separate issues being a
little conflated here:

1) Make the DWIM use a registry instead of hand-written code.

   I'm not necessarily opposed to that; certainly IWBNI plugins could
   register their own specs DWIM should try. But I don't think I'll
   be doing it (certainly not anytime soon; $freetime--). And it's
   not quite as easy as it sounds, since we have to keep track of
   which exceptions each class raises for 'no, I didn't find this' (as
   opposed to 'crap, something went wrong').

   Also, instantiating the RevSpec_* classes _doesn't_ determine
   whether the given value points at anything, it only sets up the
   object to lazily search later. We reach inside them in the _dwim
   class to evaluate on the spot (when _dwim is lazily searched) to
   find out whether the value means anything to that spectype.

   So it's not just as simple as "do this then this then this";
   encoding that in a registry will take more knowledge of what you
   can do with python than I have. As it is, the body of the dwim
   _match_on() is about 40 lines (and with my characteristic heavy use
   of whitespace and commenting, so the actual code is probably under
   25), and explicit about the complications and what's going on.

2) Have that registry be the same as the revspec_registry registry
   that the prefix:'s are searched in.

   This I would be against, because 'prefixes we recognize' and
   'things we want to DWIM' are rather different, and cramming all of
   the above info, plus ordering, plus info on which specs we even
   DWIM at all, into that registry is a giant mess of making one thing
   do two almost-totally-different things. This is mostly what I was
   conceptually arguing against in the previous message, not so much
   (1) above.

--
Matthew Fuller
<email address hidden>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2009-10-22 08:49:42 +0000
3+++ NEWS 2009-10-22 19:16:16 +0000
4@@ -233,6 +233,10 @@
5 Improvements
6 ************
7
8+* Revision specifiers can now be given in a more DWIM form, without
9+ needing explicit prefixes for specifiers like tags or revision id's.
10+ See ``bzr help revisionspec`` for full details. (Matthew Fuller)
11+
12 * Bazaar gives a warning before exiting, and writes into ``.bzr.log``, if
13 compiled extensions can't be loaded. This typically indicates a
14 packaging or installation problem. In this case Bazaar will keep
15
16=== modified file 'bzrlib/help_topics/__init__.py'
17--- bzrlib/help_topics/__init__.py 2009-10-13 20:15:45 +0000
18+++ bzrlib/help_topics/__init__.py 2009-10-22 19:16:16 +0000
19@@ -152,10 +152,16 @@
20 out.append(
21 """Revision Identifiers
22
23-A revision identifier refers to a specific state of a branch's history. It can
24-be a revision number, or a keyword followed by ':' and often other
25-parameters. Some examples of identifiers are '3', 'last:1', 'before:yesterday'
26-and 'submit:'.
27+A revision identifier refers to a specific state of a branch's history. It
28+can be expressed in several ways. It can begin with a keyword to
29+unambiguously specify a given lookup type; some examples are 'last:1',
30+'before:yesterday' and 'submit:'.
31+
32+Alternately, it can be given without a keyword, in which case it will be
33+checked as a revision number, a tag, a revision id, a date specification, or a
34+branch specification, in that order. For example, 'date:today' could be
35+written as simply 'today', though if you have a tag called 'today' that will
36+be found first.
37
38 If 'REV1' and 'REV2' are revision identifiers, then 'REV1..REV2' denotes a
39 revision range. Examples: '3647..3649', 'date:yesterday..-1' and
40
41=== modified file 'bzrlib/option.py'
42--- bzrlib/option.py 2009-04-03 20:05:25 +0000
43+++ bzrlib/option.py 2009-10-22 19:16:16 +0000
44@@ -40,25 +40,25 @@
45 each revision specifier supplied.
46
47 >>> _parse_revision_str('234')
48- [<RevisionSpec_revno 234>]
49+ [<RevisionSpec_dwim 234>]
50 >>> _parse_revision_str('234..567')
51- [<RevisionSpec_revno 234>, <RevisionSpec_revno 567>]
52+ [<RevisionSpec_dwim 234>, <RevisionSpec_dwim 567>]
53 >>> _parse_revision_str('..')
54 [<RevisionSpec None>, <RevisionSpec None>]
55 >>> _parse_revision_str('..234')
56- [<RevisionSpec None>, <RevisionSpec_revno 234>]
57+ [<RevisionSpec None>, <RevisionSpec_dwim 234>]
58 >>> _parse_revision_str('234..')
59- [<RevisionSpec_revno 234>, <RevisionSpec None>]
60+ [<RevisionSpec_dwim 234>, <RevisionSpec None>]
61 >>> _parse_revision_str('234..456..789') # Maybe this should be an error
62- [<RevisionSpec_revno 234>, <RevisionSpec_revno 456>, <RevisionSpec_revno 789>]
63+ [<RevisionSpec_dwim 234>, <RevisionSpec_dwim 456>, <RevisionSpec_dwim 789>]
64 >>> _parse_revision_str('234....789') #Error ?
65- [<RevisionSpec_revno 234>, <RevisionSpec None>, <RevisionSpec_revno 789>]
66+ [<RevisionSpec_dwim 234>, <RevisionSpec None>, <RevisionSpec_dwim 789>]
67 >>> _parse_revision_str('revid:test@other.com-234234')
68 [<RevisionSpec_revid revid:test@other.com-234234>]
69 >>> _parse_revision_str('revid:test@other.com-234234..revid:test@other.com-234235')
70 [<RevisionSpec_revid revid:test@other.com-234234>, <RevisionSpec_revid revid:test@other.com-234235>]
71 >>> _parse_revision_str('revid:test@other.com-234234..23')
72- [<RevisionSpec_revid revid:test@other.com-234234>, <RevisionSpec_revno 23>]
73+ [<RevisionSpec_revid revid:test@other.com-234234>, <RevisionSpec_dwim 23>]
74 >>> _parse_revision_str('date:2005-04-12')
75 [<RevisionSpec_date date:2005-04-12>]
76 >>> _parse_revision_str('date:2005-04-12 12:24:33')
77@@ -68,27 +68,23 @@
78 >>> _parse_revision_str('date:2005-04-12,12:24:33')
79 [<RevisionSpec_date date:2005-04-12,12:24:33>]
80 >>> _parse_revision_str('-5..23')
81- [<RevisionSpec_revno -5>, <RevisionSpec_revno 23>]
82+ [<RevisionSpec_dwim -5>, <RevisionSpec_dwim 23>]
83 >>> _parse_revision_str('-5')
84- [<RevisionSpec_revno -5>]
85+ [<RevisionSpec_dwim -5>]
86 >>> _parse_revision_str('123a')
87- Traceback (most recent call last):
88- ...
89- NoSuchRevisionSpec: No namespace registered for string: '123a'
90+ [<RevisionSpec_dwim 123a>]
91 >>> _parse_revision_str('abc')
92- Traceback (most recent call last):
93- ...
94- NoSuchRevisionSpec: No namespace registered for string: 'abc'
95+ [<RevisionSpec_dwim abc>]
96 >>> _parse_revision_str('branch:../branch2')
97 [<RevisionSpec_branch branch:../branch2>]
98 >>> _parse_revision_str('branch:../../branch2')
99 [<RevisionSpec_branch branch:../../branch2>]
100 >>> _parse_revision_str('branch:../../branch2..23')
101- [<RevisionSpec_branch branch:../../branch2>, <RevisionSpec_revno 23>]
102+ [<RevisionSpec_branch branch:../../branch2>, <RevisionSpec_dwim 23>]
103 >>> _parse_revision_str('branch:..\\\\branch2')
104 [<RevisionSpec_branch branch:..\\branch2>]
105 >>> _parse_revision_str('branch:..\\\\..\\\\branch2..23')
106- [<RevisionSpec_branch branch:..\\..\\branch2>, <RevisionSpec_revno 23>]
107+ [<RevisionSpec_branch branch:..\\..\\branch2>, <RevisionSpec_dwim 23>]
108 """
109 # TODO: Maybe move this into revisionspec.py
110 revs = []
111@@ -104,7 +100,7 @@
112 parent of the revision.
113
114 >>> _parse_change_str('123')
115- (<RevisionSpec_before before:123>, <RevisionSpec_revno 123>)
116+ (<RevisionSpec_before before:123>, <RevisionSpec_dwim 123>)
117 >>> _parse_change_str('123..124')
118 Traceback (most recent call last):
119 ...
120
121=== modified file 'bzrlib/revisionspec.py'
122--- bzrlib/revisionspec.py 2009-07-25 08:26:42 +0000
123+++ bzrlib/revisionspec.py 2009-10-22 19:16:16 +0000
124@@ -113,8 +113,6 @@
125 return RevisionInfo(branch, revno, revision_id)
126
127
128-# classes in this list should have a "prefix" attribute, against which
129-# string specs are matched
130 _revno_regex = None
131
132
133@@ -123,10 +121,10 @@
134
135 help_txt = """A parsed revision specification.
136
137- A revision specification can be an integer, in which case it is
138- assumed to be a revno (though this will translate negative values
139- into positive ones); or it can be a string, in which case it is
140- parsed for something like 'date:' or 'revid:' etc.
141+ A revision specification is a string, which may be unambiguous about
142+ what it represents by giving a prefix like 'date:' or 'revid:' etc,
143+ or it may have no prefix, in which case it's tried against several
144+ specifier types in sequence to determine what the user meant.
145
146 Revision specs are an UI element, and they have been moved out
147 of the branch class to leave "back-end" classes unaware of such
148@@ -139,6 +137,14 @@
149
150 prefix = None
151 wants_revision_history = True
152+ dwim_catchable_exceptions = (errors.InvalidRevisionSpec,)
153+ """Expections that RevisionSpec_dwim._match_on will catch.
154+
155+ If the revspec is part of dwim_revspecs, it may be tried with an invalid
156+ revspec and raise some exception. The exceptions mentioned here will not be
157+ reported to the user but simply ignored without stopping the dwim
158+ processing.
159+ """
160
161 @staticmethod
162 def from_string(spec):
163@@ -165,17 +171,9 @@
164 trace.mutter('Returning RevisionSpec %s for %s',
165 spectype.__name__, spec)
166 return spectype(spec, _internal=True)
167- # RevisionSpec_revno is special cased, because it is the only
168- # one that directly handles plain integers
169- # TODO: This should not be special cased rather it should be
170- # a method invocation on spectype.canparse()
171- global _revno_regex
172- if _revno_regex is None:
173- _revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
174- if _revno_regex.match(spec) is not None:
175- return RevisionSpec_revno(spec, _internal=True)
176-
177- raise errors.NoSuchRevisionSpec(spec)
178+ # Otherwise treat it as a DWIM, build the RevisionSpec object and
179+ # wait for _match_on to be called.
180+ return RevisionSpec_dwim(spec, _internal=True)
181
182 def __init__(self, spec, _internal=False):
183 """Create a RevisionSpec referring to the Null revision.
184@@ -290,16 +288,62 @@
185
186 # private API
187
188+class RevisionSpec_dwim(RevisionSpec):
189+ """Provides a DWIMish revision specifier lookup.
190+
191+ Note that this does not go in the revspec_registry because by definition
192+ there is no prefix to identify it. It's solely called from
193+ RevisionSpec.from_string() because the DWIMification happen when _match_on
194+ is called so the string describing the revision is kept here until needed.
195+ """
196+
197+ help_txt = None
198+ # We don't need to build the revision history ourself, that's delegated to
199+ # each revspec we try.
200+ wants_revision_history = False
201+
202+ def _try_spectype(self, rstype, branch):
203+ rs = rstype(self.spec, _internal=True)
204+ # Hit in_history to find out if it exists, or we need to try the
205+ # next type.
206+ return rs.in_history(branch)
207+
208+ def _match_on(self, branch, revs):
209+ """Run the lookup and see what we can get."""
210+
211+ # First, see if it's a revno
212+ global _revno_regex
213+ if _revno_regex is None:
214+ _revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
215+ if _revno_regex.match(self.spec) is not None:
216+ try:
217+ return self._try_spectype(RevisionSpec_revno, branch)
218+ except RevisionSpec_revno.dwim_catchable_exceptions:
219+ pass
220+
221+ # Next see what has been registered
222+ for rs_class in dwim_revspecs:
223+ try:
224+ return self._try_spectype(rs_class, branch)
225+ except rs_class.dwim_catchable_exceptions:
226+ pass
227+
228+ # Well, I dunno what it is. Note that we don't try to keep track of the
229+ # first of last exception raised during the DWIM tries as none seems
230+ # really relevant.
231+ raise errors.InvalidRevisionSpec(self.spec, branch)
232+
233+
234 class RevisionSpec_revno(RevisionSpec):
235 """Selects a revision using a number."""
236
237 help_txt = """Selects a revision using a number.
238
239 Use an integer to specify a revision in the history of the branch.
240- Optionally a branch can be specified. The 'revno:' prefix is optional.
241- A negative number will count from the end of the branch (-1 is the
242- last revision, -2 the previous one). If the negative number is larger
243- than the branch's history, the first revision is returned.
244+ Optionally a branch can be specified. A negative number will count
245+ from the end of the branch (-1 is the last revision, -2 the previous
246+ one). If the negative number is larger than the branch's history, the
247+ first revision is returned.
248 Examples::
249
250 revno:1 -> return the first revision of this branch
251@@ -561,6 +605,7 @@
252 """
253
254 prefix = 'tag:'
255+ dwim_catchable_exceptions = (errors.NoSuchTag, errors.TagsNotSupported)
256
257 def _match_on(self, branch, revs):
258 # Can raise tags not supported, NoSuchTag, etc
259@@ -760,6 +805,7 @@
260 branch:/path/to/branch
261 """
262 prefix = 'branch:'
263+ dwim_catchable_exceptions = (errors.NotBranchError,)
264
265 def _match_on(self, branch, revs):
266 from bzrlib.branch import Branch
267@@ -838,6 +884,17 @@
268 self._get_submit_location(context_branch))
269
270
271+# The order in which we want to DWIM a revision spec without any prefix.
272+# revno is always tried first and isn't listed here, this is used by
273+# RevisionSpec_dwim._match_on
274+dwim_revspecs = [
275+ RevisionSpec_tag, # Let's try for a tag
276+ RevisionSpec_revid, # Maybe it's a revid?
277+ RevisionSpec_date, # Perhaps a date?
278+ RevisionSpec_branch, # OK, last try, maybe it's a branch
279+ ]
280+
281+
282 revspec_registry = registry.Registry()
283 def _register_revspec(revspec):
284 revspec_registry.register(revspec.prefix, revspec)
285@@ -852,5 +909,7 @@
286 _register_revspec(RevisionSpec_branch)
287 _register_revspec(RevisionSpec_submit)
288
289+# classes in this list should have a "prefix" attribute, against which
290+# string specs are matched
291 SPEC_TYPES = symbol_versioning.deprecated_list(
292 symbol_versioning.deprecated_in((1, 12, 0)), "SPEC_TYPES", [])
293
294=== modified file 'bzrlib/tests/test_revisionspec.py'
295--- bzrlib/tests/test_revisionspec.py 2009-03-23 14:59:43 +0000
296+++ bzrlib/tests/test_revisionspec.py 2009-10-22 19:16:16 +0000
297@@ -146,24 +146,49 @@
298 def test_object(self):
299 self.assertRaises(TypeError, RevisionSpec.from_string, object())
300
301- def test_unregistered_spec(self):
302- self.assertRaises(errors.NoSuchRevisionSpec,
303- RevisionSpec.from_string, 'foo')
304- self.assertRaises(errors.NoSuchRevisionSpec,
305- RevisionSpec.from_string, '123a')
306-
307-
308-
309-class TestRevnoFromString(TestCase):
310-
311- def test_from_string_dotted_decimal(self):
312- self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '-1.1')
313- self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '.1')
314- self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '1..1')
315- self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '1.2..1')
316- self.assertRaises(errors.NoSuchRevisionSpec, RevisionSpec.from_string, '1.')
317- self.assertIsInstance(RevisionSpec.from_string('1.1'), RevisionSpec_revno)
318- self.assertIsInstance(RevisionSpec.from_string('1.1.3'), RevisionSpec_revno)
319+
320+class TestRevisionSpec_dwim(TestRevisionSpec):
321+
322+ # Don't need to test revno's explicitly since TRS_revno already
323+ # covers that well for us
324+ def test_dwim_spec_revno(self):
325+ self.assertInHistoryIs(2, 'r2', '2')
326+ self.assertAsRevisionId('alt_r2', '1.1.1')
327+
328+ def test_dwim_spec_revid(self):
329+ self.assertInHistoryIs(2, 'r2', 'r2')
330+
331+ def test_dwim_spec_tag(self):
332+ self.tree.branch.tags.set_tag('footag', 'r1')
333+ self.assertAsRevisionId('r1', 'footag')
334+ self.tree.branch.tags.delete_tag('footag')
335+ self.assertRaises(errors.InvalidRevisionSpec,
336+ self.get_in_history, 'footag')
337+
338+ def test_dwim_spec_tag_that_looks_like_revno(self):
339+ # Test that we slip past revno with things that look like revnos,
340+ # but aren't. Tags are convenient for testing this since we can
341+ # make them look however we want.
342+ self.tree.branch.tags.set_tag('3', 'r2')
343+ self.assertAsRevisionId('r2', '3')
344+ self.build_tree(['tree/b'])
345+ self.tree.add(['b'])
346+ self.tree.commit('b', rev_id='r3')
347+ self.assertAsRevisionId('r3', '3')
348+
349+ def test_dwim_spec_date(self):
350+ self.assertAsRevisionId('r1', 'today')
351+
352+ def test_dwim_spec_branch(self):
353+ self.assertInHistoryIs(None, 'alt_r2', 'tree2')
354+
355+ def test_dwim_spec_nonexistent(self):
356+ self.assertInvalid('somethingrandom', invalid_as_revision_id=False)
357+ self.assertInvalid('-1.1', invalid_as_revision_id=False)
358+ self.assertInvalid('.1', invalid_as_revision_id=False)
359+ self.assertInvalid('1..1', invalid_as_revision_id=False)
360+ self.assertInvalid('1.2..1', invalid_as_revision_id=False)
361+ self.assertInvalid('1.', invalid_as_revision_id=False)
362
363
364 class TestRevisionSpec_revno(TestRevisionSpec):
365
366=== modified file 'doc/en/user-guide/specifying_revisions.txt'
367--- doc/en/user-guide/specifying_revisions.txt 2009-04-04 02:57:47 +0000
368+++ doc/en/user-guide/specifying_revisions.txt 2009-10-22 19:16:16 +0000
369@@ -36,17 +36,23 @@
370 +----------------------+------------------------------------+
371 | *number* | revision number |
372 +----------------------+------------------------------------+
373- | **revno**:*number* | positive revision number |
374+ | **revno**:*number* | revision number |
375 +----------------------+------------------------------------+
376 | **last**:*number* | negative revision number |
377 +----------------------+------------------------------------+
378+ | *guid* | globally unique revision id |
379+ +----------------------+------------------------------------+
380 | **revid**:*guid* | globally unique revision id |
381 +----------------------+------------------------------------+
382 | **before**:*rev* | leftmost parent of ''rev'' |
383 +----------------------+------------------------------------+
384- | **date**:*value* | first entry after a given date |
385- +----------------------+------------------------------------+
386- | **tag**:*value* | revision matching a given tag |
387+ | *date-value* | first entry after a given date |
388+ +----------------------+------------------------------------+
389+ | **date**:*date-value*| first entry after a given date |
390+ +----------------------+------------------------------------+
391+ | *tag-name* | revision matching a given tag |
392+ +----------------------+------------------------------------+
393+ | **tag**:*tag-name* | revision matching a given tag |
394 +----------------------+------------------------------------+
395 | **ancestor**:*path* | last merged revision from a branch |
396 +----------------------+------------------------------------+