/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/revisionspec.py

  • Committer: Matthew Fuller
  • Date: 2009-08-18 08:10:44 UTC
  • mto: (4772.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 4773.
  • Revision ID: fullermd@over-yonder.net-20090818081044-2due6ius01c4pwjl
Fix up some doctests to handle things ending up as RevisionSpec_dwim's
instead of RS_revno, and ending up as _dwim's (which may error
eventually, but won't until we try to evaluate them) instead of
insta-errors.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
 
 
18
import re
 
19
 
 
20
from bzrlib.lazy_import import lazy_import
 
21
lazy_import(globals(), """
 
22
import bisect
 
23
import datetime
 
24
""")
 
25
 
 
26
from bzrlib import (
 
27
    errors,
 
28
    osutils,
 
29
    registry,
 
30
    revision,
 
31
    symbol_versioning,
 
32
    trace,
 
33
    )
 
34
 
 
35
 
 
36
_marker = []
 
37
 
 
38
 
 
39
class RevisionInfo(object):
 
40
    """The results of applying a revision specification to a branch."""
 
41
 
 
42
    help_txt = """The results of applying a revision specification to a branch.
 
43
 
 
44
    An instance has two useful attributes: revno, and rev_id.
 
45
 
 
46
    They can also be accessed as spec[0] and spec[1] respectively,
 
47
    so that you can write code like:
 
48
    revno, rev_id = RevisionSpec(branch, spec)
 
49
    although this is probably going to be deprecated later.
 
50
 
 
51
    This class exists mostly to be the return value of a RevisionSpec,
 
52
    so that you can access the member you're interested in (number or id)
 
53
    or treat the result as a tuple.
 
54
    """
 
55
 
 
56
    def __init__(self, branch, revno, rev_id=_marker):
 
57
        self.branch = branch
 
58
        self.revno = revno
 
59
        if rev_id is _marker:
 
60
            # allow caller to be lazy
 
61
            if self.revno is None:
 
62
                self.rev_id = None
 
63
            else:
 
64
                self.rev_id = branch.get_rev_id(self.revno)
 
65
        else:
 
66
            self.rev_id = rev_id
 
67
 
 
68
    def __nonzero__(self):
 
69
        # first the easy ones...
 
70
        if self.rev_id is None:
 
71
            return False
 
72
        if self.revno is not None:
 
73
            return True
 
74
        # TODO: otherwise, it should depend on how I was built -
 
75
        # if it's in_history(branch), then check revision_history(),
 
76
        # if it's in_store(branch), do the check below
 
77
        return self.branch.repository.has_revision(self.rev_id)
 
78
 
 
79
    def __len__(self):
 
80
        return 2
 
81
 
 
82
    def __getitem__(self, index):
 
83
        if index == 0: return self.revno
 
84
        if index == 1: return self.rev_id
 
85
        raise IndexError(index)
 
86
 
 
87
    def get(self):
 
88
        return self.branch.repository.get_revision(self.rev_id)
 
89
 
 
90
    def __eq__(self, other):
 
91
        if type(other) not in (tuple, list, type(self)):
 
92
            return False
 
93
        if type(other) is type(self) and self.branch is not other.branch:
 
94
            return False
 
95
        return tuple(self) == tuple(other)
 
96
 
 
97
    def __repr__(self):
 
98
        return '<bzrlib.revisionspec.RevisionInfo object %s, %s for %r>' % (
 
99
            self.revno, self.rev_id, self.branch)
 
100
 
 
101
    @staticmethod
 
102
    def from_revision_id(branch, revision_id, revs):
 
103
        """Construct a RevisionInfo given just the id.
 
104
 
 
105
        Use this if you don't know or care what the revno is.
 
106
        """
 
107
        if revision_id == revision.NULL_REVISION:
 
108
            return RevisionInfo(branch, 0, revision_id)
 
109
        try:
 
110
            revno = revs.index(revision_id) + 1
 
111
        except ValueError:
 
112
            revno = None
 
113
        return RevisionInfo(branch, revno, revision_id)
 
114
 
 
115
 
 
116
# classes in this list should have a "prefix" attribute, against which
 
117
# string specs are matched
 
118
_revno_regex = None
 
119
 
 
120
 
 
121
class RevisionSpec(object):
 
122
    """A parsed revision specification."""
 
123
 
 
124
    help_txt = """A parsed revision specification.
 
125
 
 
126
    A revision specification is a string, which may be unambiguous about
 
127
    what it represents by giving a prefix like 'date:' or 'revid:' etc,
 
128
    or it may have no prefix, in which case it's tried against several
 
129
    specifier types in sequence to determine what the user meant.
 
130
 
 
131
    Revision specs are an UI element, and they have been moved out
 
132
    of the branch class to leave "back-end" classes unaware of such
 
133
    details.  Code that gets a revno or rev_id from other code should
 
134
    not be using revision specs - revnos and revision ids are the
 
135
    accepted ways to refer to revisions internally.
 
136
 
 
137
    (Equivalent to the old Branch method get_revision_info())
 
138
    """
 
139
 
 
140
    prefix = None
 
141
    wants_revision_history = True
 
142
 
 
143
    @staticmethod
 
144
    def from_string(spec):
 
145
        """Parse a revision spec string into a RevisionSpec object.
 
146
 
 
147
        :param spec: A string specified by the user
 
148
        :return: A RevisionSpec object that understands how to parse the
 
149
            supplied notation.
 
150
        """
 
151
        if not isinstance(spec, (type(None), basestring)):
 
152
            raise TypeError('error')
 
153
 
 
154
        if spec is None:
 
155
            return RevisionSpec(None, _internal=True)
 
156
        match = revspec_registry.get_prefix(spec)
 
157
        if match is not None:
 
158
            spectype, specsuffix = match
 
159
            trace.mutter('Returning RevisionSpec %s for %s',
 
160
                         spectype.__name__, spec)
 
161
            return spectype(spec, _internal=True)
 
162
        else:
 
163
            for spectype in SPEC_TYPES:
 
164
                trace.mutter('Returning RevisionSpec %s for %s',
 
165
                             spectype.__name__, spec)
 
166
                if spec.startswith(spectype.prefix):
 
167
                    return spectype(spec, _internal=True)
 
168
            # Otherwise treat it as a DWIM
 
169
            return RevisionSpec_dwim(spec, _internal=True)
 
170
 
 
171
    def __init__(self, spec, _internal=False):
 
172
        """Create a RevisionSpec referring to the Null revision.
 
173
 
 
174
        :param spec: The original spec supplied by the user
 
175
        :param _internal: Used to ensure that RevisionSpec is not being
 
176
            called directly. Only from RevisionSpec.from_string()
 
177
        """
 
178
        if not _internal:
 
179
            # XXX: Update this after 0.10 is released
 
180
            symbol_versioning.warn('Creating a RevisionSpec directly has'
 
181
                                   ' been deprecated in version 0.11. Use'
 
182
                                   ' RevisionSpec.from_string()'
 
183
                                   ' instead.',
 
184
                                   DeprecationWarning, stacklevel=2)
 
185
        self.user_spec = spec
 
186
        if self.prefix and spec.startswith(self.prefix):
 
187
            spec = spec[len(self.prefix):]
 
188
        self.spec = spec
 
189
 
 
190
    def _match_on(self, branch, revs):
 
191
        trace.mutter('Returning RevisionSpec._match_on: None')
 
192
        return RevisionInfo(branch, None, None)
 
193
 
 
194
    def _match_on_and_check(self, branch, revs):
 
195
        info = self._match_on(branch, revs)
 
196
        if info:
 
197
            return info
 
198
        elif info == (None, None):
 
199
            # special case - nothing supplied
 
200
            return info
 
201
        elif self.prefix:
 
202
            raise errors.InvalidRevisionSpec(self.user_spec, branch)
 
203
        else:
 
204
            raise errors.InvalidRevisionSpec(self.spec, branch)
 
205
 
 
206
    def in_history(self, branch):
 
207
        if branch:
 
208
            if self.wants_revision_history:
 
209
                revs = branch.revision_history()
 
210
            else:
 
211
                revs = None
 
212
        else:
 
213
            # this should never trigger.
 
214
            # TODO: make it a deprecated code path. RBC 20060928
 
215
            revs = None
 
216
        return self._match_on_and_check(branch, revs)
 
217
 
 
218
        # FIXME: in_history is somewhat broken,
 
219
        # it will return non-history revisions in many
 
220
        # circumstances. The expected facility is that
 
221
        # in_history only returns revision-history revs,
 
222
        # in_store returns any rev. RBC 20051010
 
223
    # aliases for now, when we fix the core logic, then they
 
224
    # will do what you expect.
 
225
    in_store = in_history
 
226
    in_branch = in_store
 
227
 
 
228
    def as_revision_id(self, context_branch):
 
229
        """Return just the revision_id for this revisions spec.
 
230
 
 
231
        Some revision specs require a context_branch to be able to determine
 
232
        their value. Not all specs will make use of it.
 
233
        """
 
234
        return self._as_revision_id(context_branch)
 
235
 
 
236
    def _as_revision_id(self, context_branch):
 
237
        """Implementation of as_revision_id()
 
238
 
 
239
        Classes should override this function to provide appropriate
 
240
        functionality. The default is to just call '.in_history().rev_id'
 
241
        """
 
242
        return self.in_history(context_branch).rev_id
 
243
 
 
244
    def as_tree(self, context_branch):
 
245
        """Return the tree object for this revisions spec.
 
246
 
 
247
        Some revision specs require a context_branch to be able to determine
 
248
        the revision id and access the repository. Not all specs will make
 
249
        use of it.
 
250
        """
 
251
        return self._as_tree(context_branch)
 
252
 
 
253
    def _as_tree(self, context_branch):
 
254
        """Implementation of as_tree().
 
255
 
 
256
        Classes should override this function to provide appropriate
 
257
        functionality. The default is to just call '.as_revision_id()'
 
258
        and get the revision tree from context_branch's repository.
 
259
        """
 
260
        revision_id = self.as_revision_id(context_branch)
 
261
        return context_branch.repository.revision_tree(revision_id)
 
262
 
 
263
    def __repr__(self):
 
264
        # this is mostly for helping with testing
 
265
        return '<%s %s>' % (self.__class__.__name__,
 
266
                              self.user_spec)
 
267
 
 
268
    def needs_branch(self):
 
269
        """Whether this revision spec needs a branch.
 
270
 
 
271
        Set this to False the branch argument of _match_on is not used.
 
272
        """
 
273
        return True
 
274
 
 
275
    def get_branch(self):
 
276
        """When the revision specifier contains a branch location, return it.
 
277
 
 
278
        Otherwise, return None.
 
279
        """
 
280
        return None
 
281
 
 
282
 
 
283
# private API
 
284
 
 
285
class RevisionSpec_dwim(RevisionSpec):
 
286
    """Provides a DWIMish revision specifier lookup.
 
287
 
 
288
    Note that this does not go in the revspec_registry.  It's solely
 
289
    called from RevisionSpec.from_string().
 
290
    """
 
291
 
 
292
    help_txt = None
 
293
    # Default to False to save building the history in the revno case
 
294
    wants_revision_history = False
 
295
 
 
296
    # Util
 
297
    def __try_spectype(self, rstype, spec, branch):
 
298
        rs = rstype(spec, _internal=True)
 
299
        # Hit in_history to find out if it exists, or we need to try the
 
300
        # next type.
 
301
        return rs.in_history(branch)
 
302
 
 
303
    def _match_on(self, branch, revs):
 
304
        """Run the lookup and see what we can get."""
 
305
        spec = self.spec
 
306
 
 
307
        # First, see if it's a revno
 
308
        global _revno_regex
 
309
        if _revno_regex is None:
 
310
            _revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
 
311
        if _revno_regex.match(spec) is not None:
 
312
            try:
 
313
                return self.__try_spectype(RevisionSpec_revno, spec, branch)
 
314
            except errors.InvalidRevisionSpec:
 
315
                pass
 
316
 
 
317
        # It's not a revno, so now we need this
 
318
        self.wants_revision_history = True
 
319
 
 
320
        # OK, next let's try for a tag
 
321
        try:
 
322
            return self.__try_spectype(RevisionSpec_tag, spec, branch)
 
323
        except (errors.NoSuchTag, errors.TagsNotSupported):
 
324
            pass
 
325
 
 
326
        # Maybe it's a revid?
 
327
        try:
 
328
            return self.__try_spectype(RevisionSpec_revid, spec, branch)
 
329
        except errors.InvalidRevisionSpec:
 
330
            pass
 
331
 
 
332
        # Perhaps a date?
 
333
        try:
 
334
            return self.__try_spectype(RevisionSpec_date, spec, branch)
 
335
        except errors.InvalidRevisionSpec:
 
336
            pass
 
337
 
 
338
        # OK, last try, maybe it's a branch
 
339
        try:
 
340
            return self.__try_spectype(RevisionSpec_branch, spec, branch)
 
341
        except errors.NotBranchError:
 
342
            pass
 
343
 
 
344
        # Well, I dunno what it is.
 
345
        raise errors.InvalidRevisionSpec(self.spec, branch)
 
346
 
 
347
 
 
348
class RevisionSpec_revno(RevisionSpec):
 
349
    """Selects a revision using a number."""
 
350
 
 
351
    help_txt = """Selects a revision using a number.
 
352
 
 
353
    Use an integer to specify a revision in the history of the branch.
 
354
    Optionally a branch can be specified.  A negative number will count
 
355
    from the end of the branch (-1 is the last revision, -2 the previous
 
356
    one). If the negative number is larger than the branch's history, the
 
357
    first revision is returned.
 
358
    Examples::
 
359
 
 
360
      revno:1                   -> return the first revision of this branch
 
361
      revno:3:/path/to/branch   -> return the 3rd revision of
 
362
                                   the branch '/path/to/branch'
 
363
      revno:-1                  -> The last revision in a branch.
 
364
      -2:http://other/branch    -> The second to last revision in the
 
365
                                   remote branch.
 
366
      -1000000                  -> Most likely the first revision, unless
 
367
                                   your history is very long.
 
368
    """
 
369
    prefix = 'revno:'
 
370
    wants_revision_history = False
 
371
 
 
372
    def _match_on(self, branch, revs):
 
373
        """Lookup a revision by revision number"""
 
374
        branch, revno, revision_id = self._lookup(branch, revs)
 
375
        return RevisionInfo(branch, revno, revision_id)
 
376
 
 
377
    def _lookup(self, branch, revs_or_none):
 
378
        loc = self.spec.find(':')
 
379
        if loc == -1:
 
380
            revno_spec = self.spec
 
381
            branch_spec = None
 
382
        else:
 
383
            revno_spec = self.spec[:loc]
 
384
            branch_spec = self.spec[loc+1:]
 
385
 
 
386
        if revno_spec == '':
 
387
            if not branch_spec:
 
388
                raise errors.InvalidRevisionSpec(self.user_spec,
 
389
                        branch, 'cannot have an empty revno and no branch')
 
390
            revno = None
 
391
        else:
 
392
            try:
 
393
                revno = int(revno_spec)
 
394
                dotted = False
 
395
            except ValueError:
 
396
                # dotted decimal. This arguably should not be here
 
397
                # but the from_string method is a little primitive
 
398
                # right now - RBC 20060928
 
399
                try:
 
400
                    match_revno = tuple((int(number) for number in revno_spec.split('.')))
 
401
                except ValueError, e:
 
402
                    raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
 
403
 
 
404
                dotted = True
 
405
 
 
406
        if branch_spec:
 
407
            # the user has override the branch to look in.
 
408
            # we need to refresh the revision_history map and
 
409
            # the branch object.
 
410
            from bzrlib.branch import Branch
 
411
            branch = Branch.open(branch_spec)
 
412
            revs_or_none = None
 
413
 
 
414
        if dotted:
 
415
            try:
 
416
                revision_id = branch.dotted_revno_to_revision_id(match_revno,
 
417
                    _cache_reverse=True)
 
418
            except errors.NoSuchRevision:
 
419
                raise errors.InvalidRevisionSpec(self.user_spec, branch)
 
420
            else:
 
421
                # there is no traditional 'revno' for dotted-decimal revnos.
 
422
                # so for  API compatability we return None.
 
423
                return branch, None, revision_id
 
424
        else:
 
425
            last_revno, last_revision_id = branch.last_revision_info()
 
426
            if revno < 0:
 
427
                # if get_rev_id supported negative revnos, there would not be a
 
428
                # need for this special case.
 
429
                if (-revno) >= last_revno:
 
430
                    revno = 1
 
431
                else:
 
432
                    revno = last_revno + revno + 1
 
433
            try:
 
434
                revision_id = branch.get_rev_id(revno, revs_or_none)
 
435
            except errors.NoSuchRevision:
 
436
                raise errors.InvalidRevisionSpec(self.user_spec, branch)
 
437
        return branch, revno, revision_id
 
438
 
 
439
    def _as_revision_id(self, context_branch):
 
440
        # We would have the revno here, but we don't really care
 
441
        branch, revno, revision_id = self._lookup(context_branch, None)
 
442
        return revision_id
 
443
 
 
444
    def needs_branch(self):
 
445
        return self.spec.find(':') == -1
 
446
 
 
447
    def get_branch(self):
 
448
        if self.spec.find(':') == -1:
 
449
            return None
 
450
        else:
 
451
            return self.spec[self.spec.find(':')+1:]
 
452
 
 
453
# Old compatibility
 
454
RevisionSpec_int = RevisionSpec_revno
 
455
 
 
456
 
 
457
 
 
458
class RevisionSpec_revid(RevisionSpec):
 
459
    """Selects a revision using the revision id."""
 
460
 
 
461
    help_txt = """Selects a revision using the revision id.
 
462
 
 
463
    Supply a specific revision id, that can be used to specify any
 
464
    revision id in the ancestry of the branch.
 
465
    Including merges, and pending merges.
 
466
    Examples::
 
467
 
 
468
      revid:aaaa@bbbb-123456789 -> Select revision 'aaaa@bbbb-123456789'
 
469
    """
 
470
 
 
471
    prefix = 'revid:'
 
472
 
 
473
    def _match_on(self, branch, revs):
 
474
        # self.spec comes straight from parsing the command line arguments,
 
475
        # so we expect it to be a Unicode string. Switch it to the internal
 
476
        # representation.
 
477
        revision_id = osutils.safe_revision_id(self.spec, warn=False)
 
478
        return RevisionInfo.from_revision_id(branch, revision_id, revs)
 
479
 
 
480
    def _as_revision_id(self, context_branch):
 
481
        return osutils.safe_revision_id(self.spec, warn=False)
 
482
 
 
483
 
 
484
 
 
485
class RevisionSpec_last(RevisionSpec):
 
486
    """Selects the nth revision from the end."""
 
487
 
 
488
    help_txt = """Selects the nth revision from the end.
 
489
 
 
490
    Supply a positive number to get the nth revision from the end.
 
491
    This is the same as supplying negative numbers to the 'revno:' spec.
 
492
    Examples::
 
493
 
 
494
      last:1        -> return the last revision
 
495
      last:3        -> return the revision 2 before the end.
 
496
    """
 
497
 
 
498
    prefix = 'last:'
 
499
 
 
500
    def _match_on(self, branch, revs):
 
501
        revno, revision_id = self._revno_and_revision_id(branch, revs)
 
502
        return RevisionInfo(branch, revno, revision_id)
 
503
 
 
504
    def _revno_and_revision_id(self, context_branch, revs_or_none):
 
505
        last_revno, last_revision_id = context_branch.last_revision_info()
 
506
 
 
507
        if self.spec == '':
 
508
            if not last_revno:
 
509
                raise errors.NoCommits(context_branch)
 
510
            return last_revno, last_revision_id
 
511
 
 
512
        try:
 
513
            offset = int(self.spec)
 
514
        except ValueError, e:
 
515
            raise errors.InvalidRevisionSpec(self.user_spec, context_branch, e)
 
516
 
 
517
        if offset <= 0:
 
518
            raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
 
519
                                             'you must supply a positive value')
 
520
 
 
521
        revno = last_revno - offset + 1
 
522
        try:
 
523
            revision_id = context_branch.get_rev_id(revno, revs_or_none)
 
524
        except errors.NoSuchRevision:
 
525
            raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
 
526
        return revno, revision_id
 
527
 
 
528
    def _as_revision_id(self, context_branch):
 
529
        # We compute the revno as part of the process, but we don't really care
 
530
        # about it.
 
531
        revno, revision_id = self._revno_and_revision_id(context_branch, None)
 
532
        return revision_id
 
533
 
 
534
 
 
535
 
 
536
class RevisionSpec_before(RevisionSpec):
 
537
    """Selects the parent of the revision specified."""
 
538
 
 
539
    help_txt = """Selects the parent of the revision specified.
 
540
 
 
541
    Supply any revision spec to return the parent of that revision.  This is
 
542
    mostly useful when inspecting revisions that are not in the revision history
 
543
    of a branch.
 
544
 
 
545
    It is an error to request the parent of the null revision (before:0).
 
546
 
 
547
    Examples::
 
548
 
 
549
      before:1913    -> Return the parent of revno 1913 (revno 1912)
 
550
      before:revid:aaaa@bbbb-1234567890  -> return the parent of revision
 
551
                                            aaaa@bbbb-1234567890
 
552
      bzr diff -r before:1913..1913
 
553
            -> Find the changes between revision 1913 and its parent (1912).
 
554
               (What changes did revision 1913 introduce).
 
555
               This is equivalent to:  bzr diff -c 1913
 
556
    """
 
557
 
 
558
    prefix = 'before:'
 
559
 
 
560
    def _match_on(self, branch, revs):
 
561
        r = RevisionSpec.from_string(self.spec)._match_on(branch, revs)
 
562
        if r.revno == 0:
 
563
            raise errors.InvalidRevisionSpec(self.user_spec, branch,
 
564
                                         'cannot go before the null: revision')
 
565
        if r.revno is None:
 
566
            # We need to use the repository history here
 
567
            rev = branch.repository.get_revision(r.rev_id)
 
568
            if not rev.parent_ids:
 
569
                revno = 0
 
570
                revision_id = revision.NULL_REVISION
 
571
            else:
 
572
                revision_id = rev.parent_ids[0]
 
573
                try:
 
574
                    revno = revs.index(revision_id) + 1
 
575
                except ValueError:
 
576
                    revno = None
 
577
        else:
 
578
            revno = r.revno - 1
 
579
            try:
 
580
                revision_id = branch.get_rev_id(revno, revs)
 
581
            except errors.NoSuchRevision:
 
582
                raise errors.InvalidRevisionSpec(self.user_spec,
 
583
                                                 branch)
 
584
        return RevisionInfo(branch, revno, revision_id)
 
585
 
 
586
    def _as_revision_id(self, context_branch):
 
587
        base_revspec = RevisionSpec.from_string(self.spec)
 
588
        base_revision_id = base_revspec.as_revision_id(context_branch)
 
589
        if base_revision_id == revision.NULL_REVISION:
 
590
            raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
 
591
                                         'cannot go before the null: revision')
 
592
        context_repo = context_branch.repository
 
593
        context_repo.lock_read()
 
594
        try:
 
595
            parent_map = context_repo.get_parent_map([base_revision_id])
 
596
        finally:
 
597
            context_repo.unlock()
 
598
        if base_revision_id not in parent_map:
 
599
            # Ghost, or unknown revision id
 
600
            raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
 
601
                'cannot find the matching revision')
 
602
        parents = parent_map[base_revision_id]
 
603
        if len(parents) < 1:
 
604
            raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
 
605
                'No parents for revision.')
 
606
        return parents[0]
 
607
 
 
608
 
 
609
 
 
610
class RevisionSpec_tag(RevisionSpec):
 
611
    """Select a revision identified by tag name"""
 
612
 
 
613
    help_txt = """Selects a revision identified by a tag name.
 
614
 
 
615
    Tags are stored in the branch and created by the 'tag' command.
 
616
    """
 
617
 
 
618
    prefix = 'tag:'
 
619
 
 
620
    def _match_on(self, branch, revs):
 
621
        # Can raise tags not supported, NoSuchTag, etc
 
622
        return RevisionInfo.from_revision_id(branch,
 
623
            branch.tags.lookup_tag(self.spec),
 
624
            revs)
 
625
 
 
626
    def _as_revision_id(self, context_branch):
 
627
        return context_branch.tags.lookup_tag(self.spec)
 
628
 
 
629
 
 
630
 
 
631
class _RevListToTimestamps(object):
 
632
    """This takes a list of revisions, and allows you to bisect by date"""
 
633
 
 
634
    __slots__ = ['revs', 'branch']
 
635
 
 
636
    def __init__(self, revs, branch):
 
637
        self.revs = revs
 
638
        self.branch = branch
 
639
 
 
640
    def __getitem__(self, index):
 
641
        """Get the date of the index'd item"""
 
642
        r = self.branch.repository.get_revision(self.revs[index])
 
643
        # TODO: Handle timezone.
 
644
        return datetime.datetime.fromtimestamp(r.timestamp)
 
645
 
 
646
    def __len__(self):
 
647
        return len(self.revs)
 
648
 
 
649
 
 
650
class RevisionSpec_date(RevisionSpec):
 
651
    """Selects a revision on the basis of a datestamp."""
 
652
 
 
653
    help_txt = """Selects a revision on the basis of a datestamp.
 
654
 
 
655
    Supply a datestamp to select the first revision that matches the date.
 
656
    Date can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
657
    Matches the first entry after a given date (either at midnight or
 
658
    at a specified time).
 
659
 
 
660
    One way to display all the changes since yesterday would be::
 
661
 
 
662
        bzr log -r date:yesterday..
 
663
 
 
664
    Examples::
 
665
 
 
666
      date:yesterday            -> select the first revision since yesterday
 
667
      date:2006-08-14,17:10:14  -> select the first revision after
 
668
                                   August 14th, 2006 at 5:10pm.
 
669
    """
 
670
    prefix = 'date:'
 
671
    _date_re = re.compile(
 
672
            r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
673
            r'(,|T)?\s*'
 
674
            r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
675
        )
 
676
 
 
677
    def _match_on(self, branch, revs):
 
678
        """Spec for date revisions:
 
679
          date:value
 
680
          value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
681
          matches the first entry after a given date (either at midnight or
 
682
          at a specified time).
 
683
        """
 
684
        #  XXX: This doesn't actually work
 
685
        #  So the proper way of saying 'give me all entries for today' is:
 
686
        #      -r date:yesterday..date:today
 
687
        today = datetime.datetime.fromordinal(datetime.date.today().toordinal())
 
688
        if self.spec.lower() == 'yesterday':
 
689
            dt = today - datetime.timedelta(days=1)
 
690
        elif self.spec.lower() == 'today':
 
691
            dt = today
 
692
        elif self.spec.lower() == 'tomorrow':
 
693
            dt = today + datetime.timedelta(days=1)
 
694
        else:
 
695
            m = self._date_re.match(self.spec)
 
696
            if not m or (not m.group('date') and not m.group('time')):
 
697
                raise errors.InvalidRevisionSpec(self.user_spec,
 
698
                                                 branch, 'invalid date')
 
699
 
 
700
            try:
 
701
                if m.group('date'):
 
702
                    year = int(m.group('year'))
 
703
                    month = int(m.group('month'))
 
704
                    day = int(m.group('day'))
 
705
                else:
 
706
                    year = today.year
 
707
                    month = today.month
 
708
                    day = today.day
 
709
 
 
710
                if m.group('time'):
 
711
                    hour = int(m.group('hour'))
 
712
                    minute = int(m.group('minute'))
 
713
                    if m.group('second'):
 
714
                        second = int(m.group('second'))
 
715
                    else:
 
716
                        second = 0
 
717
                else:
 
718
                    hour, minute, second = 0,0,0
 
719
            except ValueError:
 
720
                raise errors.InvalidRevisionSpec(self.user_spec,
 
721
                                                 branch, 'invalid date')
 
722
 
 
723
            dt = datetime.datetime(year=year, month=month, day=day,
 
724
                    hour=hour, minute=minute, second=second)
 
725
        branch.lock_read()
 
726
        try:
 
727
            rev = bisect.bisect(_RevListToTimestamps(revs, branch), dt)
 
728
        finally:
 
729
            branch.unlock()
 
730
        if rev == len(revs):
 
731
            raise errors.InvalidRevisionSpec(self.user_spec, branch)
 
732
        else:
 
733
            return RevisionInfo(branch, rev + 1)
 
734
 
 
735
 
 
736
 
 
737
class RevisionSpec_ancestor(RevisionSpec):
 
738
    """Selects a common ancestor with a second branch."""
 
739
 
 
740
    help_txt = """Selects a common ancestor with a second branch.
 
741
 
 
742
    Supply the path to a branch to select the common ancestor.
 
743
 
 
744
    The common ancestor is the last revision that existed in both
 
745
    branches. Usually this is the branch point, but it could also be
 
746
    a revision that was merged.
 
747
 
 
748
    This is frequently used with 'diff' to return all of the changes
 
749
    that your branch introduces, while excluding the changes that you
 
750
    have not merged from the remote branch.
 
751
 
 
752
    Examples::
 
753
 
 
754
      ancestor:/path/to/branch
 
755
      $ bzr diff -r ancestor:../../mainline/branch
 
756
    """
 
757
    prefix = 'ancestor:'
 
758
 
 
759
    def _match_on(self, branch, revs):
 
760
        trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
 
761
        return self._find_revision_info(branch, self.spec)
 
762
 
 
763
    def _as_revision_id(self, context_branch):
 
764
        return self._find_revision_id(context_branch, self.spec)
 
765
 
 
766
    @staticmethod
 
767
    def _find_revision_info(branch, other_location):
 
768
        revision_id = RevisionSpec_ancestor._find_revision_id(branch,
 
769
                                                              other_location)
 
770
        try:
 
771
            revno = branch.revision_id_to_revno(revision_id)
 
772
        except errors.NoSuchRevision:
 
773
            revno = None
 
774
        return RevisionInfo(branch, revno, revision_id)
 
775
 
 
776
    @staticmethod
 
777
    def _find_revision_id(branch, other_location):
 
778
        from bzrlib.branch import Branch
 
779
 
 
780
        branch.lock_read()
 
781
        try:
 
782
            revision_a = revision.ensure_null(branch.last_revision())
 
783
            if revision_a == revision.NULL_REVISION:
 
784
                raise errors.NoCommits(branch)
 
785
            if other_location == '':
 
786
                other_location = branch.get_parent()
 
787
            other_branch = Branch.open(other_location)
 
788
            other_branch.lock_read()
 
789
            try:
 
790
                revision_b = revision.ensure_null(other_branch.last_revision())
 
791
                if revision_b == revision.NULL_REVISION:
 
792
                    raise errors.NoCommits(other_branch)
 
793
                graph = branch.repository.get_graph(other_branch.repository)
 
794
                rev_id = graph.find_unique_lca(revision_a, revision_b)
 
795
            finally:
 
796
                other_branch.unlock()
 
797
            if rev_id == revision.NULL_REVISION:
 
798
                raise errors.NoCommonAncestor(revision_a, revision_b)
 
799
            return rev_id
 
800
        finally:
 
801
            branch.unlock()
 
802
 
 
803
 
 
804
 
 
805
 
 
806
class RevisionSpec_branch(RevisionSpec):
 
807
    """Selects the last revision of a specified branch."""
 
808
 
 
809
    help_txt = """Selects the last revision of a specified branch.
 
810
 
 
811
    Supply the path to a branch to select its last revision.
 
812
 
 
813
    Examples::
 
814
 
 
815
      branch:/path/to/branch
 
816
    """
 
817
    prefix = 'branch:'
 
818
 
 
819
    def _match_on(self, branch, revs):
 
820
        from bzrlib.branch import Branch
 
821
        other_branch = Branch.open(self.spec)
 
822
        revision_b = other_branch.last_revision()
 
823
        if revision_b in (None, revision.NULL_REVISION):
 
824
            raise errors.NoCommits(other_branch)
 
825
        # pull in the remote revisions so we can diff
 
826
        branch.fetch(other_branch, revision_b)
 
827
        try:
 
828
            revno = branch.revision_id_to_revno(revision_b)
 
829
        except errors.NoSuchRevision:
 
830
            revno = None
 
831
        return RevisionInfo(branch, revno, revision_b)
 
832
 
 
833
    def _as_revision_id(self, context_branch):
 
834
        from bzrlib.branch import Branch
 
835
        other_branch = Branch.open(self.spec)
 
836
        last_revision = other_branch.last_revision()
 
837
        last_revision = revision.ensure_null(last_revision)
 
838
        context_branch.fetch(other_branch, last_revision)
 
839
        if last_revision == revision.NULL_REVISION:
 
840
            raise errors.NoCommits(other_branch)
 
841
        return last_revision
 
842
 
 
843
    def _as_tree(self, context_branch):
 
844
        from bzrlib.branch import Branch
 
845
        other_branch = Branch.open(self.spec)
 
846
        last_revision = other_branch.last_revision()
 
847
        last_revision = revision.ensure_null(last_revision)
 
848
        if last_revision == revision.NULL_REVISION:
 
849
            raise errors.NoCommits(other_branch)
 
850
        return other_branch.repository.revision_tree(last_revision)
 
851
 
 
852
 
 
853
 
 
854
class RevisionSpec_submit(RevisionSpec_ancestor):
 
855
    """Selects a common ancestor with a submit branch."""
 
856
 
 
857
    help_txt = """Selects a common ancestor with the submit branch.
 
858
 
 
859
    Diffing against this shows all the changes that were made in this branch,
 
860
    and is a good predictor of what merge will do.  The submit branch is
 
861
    used by the bundle and merge directive commands.  If no submit branch
 
862
    is specified, the parent branch is used instead.
 
863
 
 
864
    The common ancestor is the last revision that existed in both
 
865
    branches. Usually this is the branch point, but it could also be
 
866
    a revision that was merged.
 
867
 
 
868
    Examples::
 
869
 
 
870
      $ bzr diff -r submit:
 
871
    """
 
872
 
 
873
    prefix = 'submit:'
 
874
 
 
875
    def _get_submit_location(self, branch):
 
876
        submit_location = branch.get_submit_branch()
 
877
        location_type = 'submit branch'
 
878
        if submit_location is None:
 
879
            submit_location = branch.get_parent()
 
880
            location_type = 'parent branch'
 
881
        if submit_location is None:
 
882
            raise errors.NoSubmitBranch(branch)
 
883
        trace.note('Using %s %s', location_type, submit_location)
 
884
        return submit_location
 
885
 
 
886
    def _match_on(self, branch, revs):
 
887
        trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
 
888
        return self._find_revision_info(branch,
 
889
            self._get_submit_location(branch))
 
890
 
 
891
    def _as_revision_id(self, context_branch):
 
892
        return self._find_revision_id(context_branch,
 
893
            self._get_submit_location(context_branch))
 
894
 
 
895
 
 
896
revspec_registry = registry.Registry()
 
897
def _register_revspec(revspec):
 
898
    revspec_registry.register(revspec.prefix, revspec)
 
899
 
 
900
_register_revspec(RevisionSpec_revno)
 
901
_register_revspec(RevisionSpec_revid)
 
902
_register_revspec(RevisionSpec_last)
 
903
_register_revspec(RevisionSpec_before)
 
904
_register_revspec(RevisionSpec_tag)
 
905
_register_revspec(RevisionSpec_date)
 
906
_register_revspec(RevisionSpec_ancestor)
 
907
_register_revspec(RevisionSpec_branch)
 
908
_register_revspec(RevisionSpec_submit)
 
909
 
 
910
SPEC_TYPES = symbol_versioning.deprecated_list(
 
911
    symbol_versioning.deprecated_in((1, 12, 0)), "SPEC_TYPES", [])