~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/revisionspec.py

  • Committer: Vincent Ladeuil
  • Date: 2010-02-11 09:27:55 UTC
  • mfrom: (5017.3.46 test-servers)
  • mto: This revision was merged to the branch mainline in revision 5030.
  • Revision ID: v.ladeuil+lp@free.fr-20100211092755-3vvu4vbwiwjjte3s
Move tests servers from bzrlib.transport to bzrlib.tests.test_server

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