14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
20
20
from bzrlib.lazy_import import lazy_import
21
21
lazy_import(globals(), """
26
branch as _mod_branch,
32
from bzrlib.i18n import gettext
26
35
from bzrlib import (
39
43
class RevisionInfo(object):
40
44
"""The results of applying a revision specification to a branch."""
53
57
or treat the result as a tuple.
56
def __init__(self, branch, revno, rev_id=_marker):
60
def __init__(self, branch, revno=None, rev_id=None):
57
61
self.branch = branch
62
self._has_revno = (revno is not None)
65
if self.rev_id is None and self._revno is not None:
60
66
# allow caller to be lazy
61
if self.revno is None:
64
self.rev_id = branch.get_rev_id(self.revno)
67
self.rev_id = branch.get_rev_id(self._revno)
71
if not self._has_revno and self.rev_id is not None:
73
self._revno = self.branch.revision_id_to_revno(self.rev_id)
74
except errors.NoSuchRevision:
76
self._has_revno = True
68
79
def __nonzero__(self):
69
# first the easy ones...
70
80
if self.rev_id is None:
72
if self.revno is not None:
74
82
# TODO: otherwise, it should depend on how I was built -
75
83
# if it's in_history(branch), then check revision_history(),
76
84
# if it's in_store(branch), do the check below
99
107
self.revno, self.rev_id, self.branch)
102
def from_revision_id(branch, revision_id, revs):
110
def from_revision_id(branch, revision_id, revs=symbol_versioning.DEPRECATED_PARAMETER):
103
111
"""Construct a RevisionInfo given just the id.
105
113
Use this if you don't know or care what the revno is.
107
if revision_id == revision.NULL_REVISION:
108
return RevisionInfo(branch, 0, revision_id)
110
revno = revs.index(revision_id) + 1
113
return RevisionInfo(branch, revno, revision_id)
116
# classes in this list should have a "prefix" attribute, against which
117
# string specs are matched
115
if symbol_versioning.deprecated_passed(revs):
116
symbol_versioning.warn(
117
'RevisionInfo.from_revision_id(revs) was deprecated in 2.5.',
120
return RevisionInfo(branch, revno=None, rev_id=revision_id)
121
123
class RevisionSpec(object):
124
126
help_txt = """A parsed revision specification.
126
A revision specification can be an integer, in which case it is
127
assumed to be a revno (though this will translate negative values
128
into positive ones); or it can be a string, in which case it is
129
parsed for something like 'date:' or 'revid:' etc.
128
A revision specification is a string, which may be unambiguous about
129
what it represents by giving a prefix like 'date:' or 'revid:' etc,
130
or it may have no prefix, in which case it's tried against several
131
specifier types in sequence to determine what the user meant.
131
133
Revision specs are an UI element, and they have been moved out
132
134
of the branch class to leave "back-end" classes unaware of such
141
wants_revision_history = True
143
# wants_revision_history has been deprecated in 2.5.
144
wants_revision_history = False
145
dwim_catchable_exceptions = (errors.InvalidRevisionSpec,)
146
"""Exceptions that RevisionSpec_dwim._match_on will catch.
148
If the revspec is part of ``dwim_revspecs``, it may be tried with an
149
invalid revspec and raises some exception. The exceptions mentioned here
150
will not be reported to the user but simply ignored without stopping the
144
155
def from_string(spec):
160
171
spectype.__name__, spec)
161
172
return spectype(spec, _internal=True)
163
for spectype in SPEC_TYPES:
164
if spec.startswith(spectype.prefix):
165
trace.mutter('Returning RevisionSpec %s for %s',
166
spectype.__name__, spec)
167
return spectype(spec, _internal=True)
168
# RevisionSpec_revno is special cased, because it is the only
169
# one that directly handles plain integers
170
# TODO: This should not be special cased rather it should be
171
# a method invocation on spectype.canparse()
173
if _revno_regex is None:
174
_revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
175
if _revno_regex.match(spec) is not None:
176
return RevisionSpec_revno(spec, _internal=True)
178
raise errors.NoSuchRevisionSpec(spec)
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)
180
178
def __init__(self, spec, _internal=False):
181
179
"""Create a RevisionSpec referring to the Null revision.
214
212
def in_history(self, branch):
216
214
if self.wants_revision_history:
217
revs = branch.revision_history()
215
symbol_versioning.warn(
216
"RevisionSpec.wants_revision_history was "
217
"deprecated in 2.5 (%s)." % self.__class__.__name__,
221
graph = branch.repository.get_graph()
222
revs = list(graph.iter_lefthand_ancestry(
223
branch.last_revision(), [revision.NULL_REVISION]))
302
class RevisionSpec_dwim(RevisionSpec):
303
"""Provides a DWIMish revision specifier lookup.
305
Note that this does not go in the revspec_registry because by definition
306
there is no prefix to identify it. It's solely called from
307
RevisionSpec.from_string() because the DWIMification happen when _match_on
308
is called so the string describing the revision is kept here until needed.
313
_revno_regex = lazy_regex.lazy_compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
315
# The revspecs to try
316
_possible_revspecs = []
318
def _try_spectype(self, rstype, branch):
319
rs = rstype(self.spec, _internal=True)
320
# Hit in_history to find out if it exists, or we need to try the
322
return rs.in_history(branch)
324
def _match_on(self, branch, revs):
325
"""Run the lookup and see what we can get."""
327
# First, see if it's a revno
328
if self._revno_regex.match(self.spec) is not None:
330
return self._try_spectype(RevisionSpec_revno, branch)
331
except RevisionSpec_revno.dwim_catchable_exceptions:
334
# Next see what has been registered
335
for objgetter in self._possible_revspecs:
336
rs_class = objgetter.get_obj()
338
return self._try_spectype(rs_class, branch)
339
except rs_class.dwim_catchable_exceptions:
342
# Try the old (deprecated) dwim list:
343
for rs_class in dwim_revspecs:
345
return self._try_spectype(rs_class, branch)
346
except rs_class.dwim_catchable_exceptions:
349
# Well, I dunno what it is. Note that we don't try to keep track of the
350
# first of last exception raised during the DWIM tries as none seems
352
raise errors.InvalidRevisionSpec(self.spec, branch)
355
def append_possible_revspec(cls, revspec):
356
"""Append a possible DWIM revspec.
358
:param revspec: Revision spec to try.
360
cls._possible_revspecs.append(registry._ObjectGetter(revspec))
363
def append_possible_lazy_revspec(cls, module_name, member_name):
364
"""Append a possible lazily loaded DWIM revspec.
366
:param module_name: Name of the module with the revspec
367
:param member_name: Name of the revspec within the module
369
cls._possible_revspecs.append(
370
registry._LazyObjectGetter(module_name, member_name))
293
373
class RevisionSpec_revno(RevisionSpec):
294
374
"""Selects a revision using a number."""
296
376
help_txt = """Selects a revision using a number.
298
378
Use an integer to specify a revision in the history of the branch.
299
Optionally a branch can be specified. The 'revno:' prefix is optional.
300
A negative number will count from the end of the branch (-1 is the
301
last revision, -2 the previous one). If the negative number is larger
302
than the branch's history, the first revision is returned.
379
Optionally a branch can be specified. A negative number will count
380
from the end of the branch (-1 is the last revision, -2 the previous
381
one). If the negative number is larger than the branch's history, the
382
first revision is returned.
305
385
revno:1 -> return the first revision of this branch
312
392
your history is very long.
314
394
prefix = 'revno:'
315
wants_revision_history = False
317
396
def _match_on(self, branch, revs):
318
397
"""Lookup a revision by revision number"""
319
branch, revno, revision_id = self._lookup(branch, revs)
398
branch, revno, revision_id = self._lookup(branch)
320
399
return RevisionInfo(branch, revno, revision_id)
322
def _lookup(self, branch, revs_or_none):
401
def _lookup(self, branch):
323
402
loc = self.spec.find(':')
325
404
revno_spec = self.spec
377
452
revno = last_revno + revno + 1
379
revision_id = branch.get_rev_id(revno, revs_or_none)
454
revision_id = branch.get_rev_id(revno)
380
455
except errors.NoSuchRevision:
381
456
raise errors.InvalidRevisionSpec(self.user_spec, branch)
382
457
return branch, revno, revision_id
384
459
def _as_revision_id(self, context_branch):
385
460
# We would have the revno here, but we don't really care
386
branch, revno, revision_id = self._lookup(context_branch, None)
461
branch, revno, revision_id = self._lookup(context_branch)
387
462
return revision_id
389
464
def needs_branch(self):
399
474
RevisionSpec_int = RevisionSpec_revno
403
class RevisionSpec_revid(RevisionSpec):
477
class RevisionIDSpec(RevisionSpec):
479
def _match_on(self, branch, revs):
480
revision_id = self.as_revision_id(branch)
481
return RevisionInfo.from_revision_id(branch, revision_id)
484
class RevisionSpec_revid(RevisionIDSpec):
404
485
"""Selects a revision using the revision id."""
406
487
help_txt = """Selects a revision using the revision id.
416
497
prefix = 'revid:'
418
def _match_on(self, branch, revs):
499
def _as_revision_id(self, context_branch):
419
500
# self.spec comes straight from parsing the command line arguments,
420
501
# so we expect it to be a Unicode string. Switch it to the internal
421
502
# representation.
422
revision_id = osutils.safe_revision_id(self.spec, warn=False)
423
return RevisionInfo.from_revision_id(branch, revision_id, revs)
425
def _as_revision_id(self, context_branch):
426
503
return osutils.safe_revision_id(self.spec, warn=False)
445
522
def _match_on(self, branch, revs):
446
revno, revision_id = self._revno_and_revision_id(branch, revs)
523
revno, revision_id = self._revno_and_revision_id(branch)
447
524
return RevisionInfo(branch, revno, revision_id)
449
def _revno_and_revision_id(self, context_branch, revs_or_none):
526
def _revno_and_revision_id(self, context_branch):
450
527
last_revno, last_revision_id = context_branch.last_revision_info()
452
529
if self.spec == '':
529
602
return RevisionInfo(branch, revno, revision_id)
531
604
def _as_revision_id(self, context_branch):
532
base_revspec = RevisionSpec.from_string(self.spec)
533
base_revision_id = base_revspec.as_revision_id(context_branch)
605
base_revision_id = RevisionSpec.from_string(self.spec)._as_revision_id(context_branch)
534
606
if base_revision_id == revision.NULL_REVISION:
535
607
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
536
608
'cannot go before the null: revision')
636
dwim_catchable_exceptions = (errors.NoSuchTag, errors.TagsNotSupported)
565
638
def _match_on(self, branch, revs):
566
639
# Can raise tags not supported, NoSuchTag, etc
567
640
return RevisionInfo.from_revision_id(branch,
568
branch.tags.lookup_tag(self.spec),
641
branch.tags.lookup_tag(self.spec))
571
643
def _as_revision_id(self, context_branch):
572
644
return context_branch.tags.lookup_tag(self.spec)
576
648
class _RevListToTimestamps(object):
577
649
"""This takes a list of revisions, and allows you to bisect by date"""
579
__slots__ = ['revs', 'branch']
651
__slots__ = ['branch']
581
def __init__(self, revs, branch):
653
def __init__(self, branch):
583
654
self.branch = branch
585
656
def __getitem__(self, index):
586
657
"""Get the date of the index'd item"""
587
r = self.branch.repository.get_revision(self.revs[index])
658
r = self.branch.repository.get_revision(self.branch.get_rev_id(index))
588
659
# TODO: Handle timezone.
589
660
return datetime.datetime.fromtimestamp(r.timestamp)
591
662
def __len__(self):
592
return len(self.revs)
663
return self.branch.revno()
595
666
class RevisionSpec_date(RevisionSpec):
669
740
hour=hour, minute=minute, second=second)
670
741
branch.lock_read()
672
rev = bisect.bisect(_RevListToTimestamps(revs, branch), dt)
743
rev = bisect.bisect(_RevListToTimestamps(branch), dt, 1)
746
if rev == branch.revno():
676
747
raise errors.InvalidRevisionSpec(self.user_spec, branch)
678
return RevisionInfo(branch, rev + 1)
748
return RevisionInfo(branch, rev)
712
782
def _find_revision_info(branch, other_location):
713
783
revision_id = RevisionSpec_ancestor._find_revision_id(branch,
716
revno = branch.revision_id_to_revno(revision_id)
717
except errors.NoSuchRevision:
719
return RevisionInfo(branch, revno, revision_id)
785
return RevisionInfo(branch, None, revision_id)
722
788
def _find_revision_id(branch, other_location):
767
834
revision_b = other_branch.last_revision()
768
835
if revision_b in (None, revision.NULL_REVISION):
769
836
raise errors.NoCommits(other_branch)
770
# pull in the remote revisions so we can diff
771
branch.fetch(other_branch, revision_b)
773
revno = branch.revision_id_to_revno(revision_b)
774
except errors.NoSuchRevision:
776
return RevisionInfo(branch, revno, revision_b)
838
branch = other_branch
841
# pull in the remote revisions so we can diff
842
branch.fetch(other_branch, revision_b)
843
except errors.ReadOnlyError:
844
branch = other_branch
845
return RevisionInfo(branch, None, revision_b)
778
847
def _as_revision_id(self, context_branch):
779
848
from bzrlib.branch import Branch
838
914
self._get_submit_location(context_branch))
917
class RevisionSpec_annotate(RevisionIDSpec):
921
help_txt = """Select the revision that last modified the specified line.
923
Select the revision that last modified the specified line. Line is
924
specified as path:number. Path is a relative path to the file. Numbers
925
start at 1, and are relative to the current version, not the last-
926
committed version of the file.
929
def _raise_invalid(self, numstring, context_branch):
930
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
931
'No such line: %s' % numstring)
933
def _as_revision_id(self, context_branch):
934
path, numstring = self.spec.rsplit(':', 1)
936
index = int(numstring) - 1
938
self._raise_invalid(numstring, context_branch)
939
tree, file_path = workingtree.WorkingTree.open_containing(path)
942
file_id = tree.path2id(file_path)
944
raise errors.InvalidRevisionSpec(self.user_spec,
945
context_branch, "File '%s' is not versioned." %
947
revision_ids = [r for (r, l) in tree.annotate_iter(file_id)]
951
revision_id = revision_ids[index]
953
self._raise_invalid(numstring, context_branch)
954
if revision_id == revision.CURRENT_REVISION:
955
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
956
'Line %s has not been committed.' % numstring)
960
class RevisionSpec_mainline(RevisionIDSpec):
962
help_txt = """Select mainline revision that merged the specified revision.
964
Select the revision that merged the specified revision into mainline.
969
def _as_revision_id(self, context_branch):
970
revspec = RevisionSpec.from_string(self.spec)
971
if revspec.get_branch() is None:
972
spec_branch = context_branch
974
spec_branch = _mod_branch.Branch.open(revspec.get_branch())
975
revision_id = revspec.as_revision_id(spec_branch)
976
graph = context_branch.repository.get_graph()
977
result = graph.find_lefthand_merger(revision_id,
978
context_branch.last_revision())
980
raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
984
# The order in which we want to DWIM a revision spec without any prefix.
985
# revno is always tried first and isn't listed here, this is used by
986
# RevisionSpec_dwim._match_on
987
dwim_revspecs = symbol_versioning.deprecated_list(
988
symbol_versioning.deprecated_in((2, 4, 0)), "dwim_revspecs", [])
990
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_tag)
991
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_revid)
992
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_date)
993
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_branch)
841
995
revspec_registry = registry.Registry()
842
996
def _register_revspec(revspec):
843
997
revspec_registry.register(revspec.prefix, revspec)