~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/revisionspec.py

Refactored the export code to make it easier to add new export formats.

Show diffs side-by-side

added added

removed removed

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