~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/revisionspec.py

Add bzrlib.tests.per_repository_vf.

Show diffs side-by-side

added added

removed removed

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