/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 breezy/revisionspec.py

  • Committer: Martin
  • Date: 2017-06-10 01:57:00 UTC
  • mto: This revision was merged to the branch mainline in revision 6679.
  • Revision ID: gzlist@googlemail.com-20170610015700-o3xeuyaqry2obiay
Go back to native str for urls and many other py3 changes

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