1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
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.
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.
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
from bzrlib.lazy_import import lazy_import
21
lazy_import(globals(), """
38
class RevisionInfo(object):
39
"""The results of applying a revision specification to a branch."""
41
help_txt = """The results of applying a revision specification to a branch.
43
An instance has two useful attributes: revno, and rev_id.
45
They can also be accessed as spec[0] and spec[1] respectively,
46
so that you can write code like:
47
revno, rev_id = RevisionSpec(branch, spec)
48
although this is probably going to be deprecated later.
50
This class exists mostly to be the return value of a RevisionSpec,
51
so that you can access the member you're interested in (number or id)
52
or treat the result as a tuple.
55
def __init__(self, branch, revno, rev_id=_marker):
59
# allow caller to be lazy
60
if self.revno is None:
63
self.rev_id = branch.get_rev_id(self.revno)
67
def __nonzero__(self):
68
# first the easy ones...
69
if self.rev_id is None:
71
if self.revno is not None:
73
# TODO: otherwise, it should depend on how I was built -
74
# if it's in_history(branch), then check revision_history(),
75
# if it's in_store(branch), do the check below
76
return self.branch.repository.has_revision(self.rev_id)
81
def __getitem__(self, index):
82
if index == 0: return self.revno
83
if index == 1: return self.rev_id
84
raise IndexError(index)
87
return self.branch.repository.get_revision(self.rev_id)
89
def __eq__(self, other):
90
if type(other) not in (tuple, list, type(self)):
92
if type(other) is type(self) and self.branch is not other.branch:
94
return tuple(self) == tuple(other)
97
return '<bzrlib.revisionspec.RevisionInfo object %s, %s for %r>' % (
98
self.revno, self.rev_id, self.branch)
101
def from_revision_id(branch, revision_id, revs):
102
"""Construct a RevisionInfo given just the id.
104
Use this if you don't know or care what the revno is.
106
if revision_id == revision.NULL_REVISION:
107
return RevisionInfo(branch, 0, revision_id)
109
revno = revs.index(revision_id) + 1
112
return RevisionInfo(branch, revno, revision_id)
115
# classes in this list should have a "prefix" attribute, against which
116
# string specs are matched
121
class RevisionSpec(object):
122
"""A parsed revision specification."""
124
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.
131
Revision specs are an UI element, and they have been moved out
132
of the branch class to leave "back-end" classes unaware of such
133
details. Code that gets a revno or rev_id from other code should
134
not be using revision specs - revnos and revision ids are the
135
accepted ways to refer to revisions internally.
137
(Equivalent to the old Branch method get_revision_info())
141
wants_revision_history = True
143
def __new__(cls, spec, _internal=False):
145
return object.__new__(cls, spec, _internal=_internal)
147
symbol_versioning.warn('Creating a RevisionSpec directly has'
148
' been deprecated in version 0.11. Use'
149
' RevisionSpec.from_string()'
151
DeprecationWarning, stacklevel=2)
152
return RevisionSpec.from_string(spec)
155
def from_string(spec):
156
"""Parse a revision spec string into a RevisionSpec object.
158
:param spec: A string specified by the user
159
:return: A RevisionSpec object that understands how to parse the
162
if not isinstance(spec, (type(None), basestring)):
163
raise TypeError('error')
166
return RevisionSpec(None, _internal=True)
167
for spectype in SPEC_TYPES:
168
if spec.startswith(spectype.prefix):
169
trace.mutter('Returning RevisionSpec %s for %s',
170
spectype.__name__, spec)
171
return spectype(spec, _internal=True)
173
# RevisionSpec_revno is special cased, because it is the only
174
# one that directly handles plain integers
175
# TODO: This should not be special cased rather it should be
176
# a method invocation on spectype.canparse()
178
if _revno_regex is None:
179
_revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
180
if _revno_regex.match(spec) is not None:
181
return RevisionSpec_revno(spec, _internal=True)
183
raise errors.NoSuchRevisionSpec(spec)
185
def __init__(self, spec, _internal=False):
186
"""Create a RevisionSpec referring to the Null revision.
188
:param spec: The original spec supplied by the user
189
:param _internal: Used to ensure that RevisionSpec is not being
190
called directly. Only from RevisionSpec.from_string()
193
# XXX: Update this after 0.10 is released
194
symbol_versioning.warn('Creating a RevisionSpec directly has'
195
' been deprecated in version 0.11. Use'
196
' RevisionSpec.from_string()'
198
DeprecationWarning, stacklevel=2)
199
self.user_spec = spec
200
if self.prefix and spec.startswith(self.prefix):
201
spec = spec[len(self.prefix):]
204
def _match_on(self, branch, revs):
205
trace.mutter('Returning RevisionSpec._match_on: None')
206
return RevisionInfo(branch, None, None)
208
def _match_on_and_check(self, branch, revs):
209
info = self._match_on(branch, revs)
212
elif info == (None, None):
213
# special case - nothing supplied
216
raise errors.InvalidRevisionSpec(self.user_spec, branch)
218
raise errors.InvalidRevisionSpec(self.spec, branch)
220
def in_history(self, branch):
222
if self.wants_revision_history:
223
revs = branch.revision_history()
227
# this should never trigger.
228
# TODO: make it a deprecated code path. RBC 20060928
230
return self._match_on_and_check(branch, revs)
232
# FIXME: in_history is somewhat broken,
233
# it will return non-history revisions in many
234
# circumstances. The expected facility is that
235
# in_history only returns revision-history revs,
236
# in_store returns any rev. RBC 20051010
237
# aliases for now, when we fix the core logic, then they
238
# will do what you expect.
239
in_store = in_history
242
def as_revision_id(self, context_branch):
243
"""Return just the revision_id for this revisions spec.
245
Some revision specs require a context_branch to be able to determine
246
their value. Not all specs will make use of it.
248
return self._as_revision_id(context_branch)
250
def _as_revision_id(self, context_branch):
251
"""Implementation of as_revision_id()
253
Classes should override this function to provide appropriate
254
functionality. The default is to just call '.in_history().rev_id'
256
return self.in_history(context_branch).rev_id
259
# this is mostly for helping with testing
260
return '<%s %s>' % (self.__class__.__name__,
263
def needs_branch(self):
264
"""Whether this revision spec needs a branch.
266
Set this to False the branch argument of _match_on is not used.
270
def get_branch(self):
271
"""When the revision specifier contains a branch location, return it.
273
Otherwise, return None.
280
class RevisionSpec_revno(RevisionSpec):
281
"""Selects a revision using a number."""
283
help_txt = """Selects a revision using a number.
285
Use an integer to specify a revision in the history of the branch.
286
Optionally a branch can be specified. The 'revno:' prefix is optional.
287
A negative number will count from the end of the branch (-1 is the
288
last revision, -2 the previous one). If the negative number is larger
289
than the branch's history, the first revision is returned.
292
revno:1 -> return the first revision
293
revno:3:/path/to/branch -> return the 3rd revision of
294
the branch '/path/to/branch'
295
revno:-1 -> The last revision in a branch.
296
-2:http://other/branch -> The second to last revision in the
298
-1000000 -> Most likely the first revision, unless
299
your history is very long.
302
wants_revision_history = False
304
def _match_on(self, branch, revs):
305
"""Lookup a revision by revision number"""
306
branch, revno, revision_id = self._lookup(branch, revs)
307
return RevisionInfo(branch, revno, revision_id)
309
def _lookup(self, branch, revs_or_none):
310
loc = self.spec.find(':')
312
revno_spec = self.spec
315
revno_spec = self.spec[:loc]
316
branch_spec = self.spec[loc+1:]
320
raise errors.InvalidRevisionSpec(self.user_spec,
321
branch, 'cannot have an empty revno and no branch')
325
revno = int(revno_spec)
328
# dotted decimal. This arguably should not be here
329
# but the from_string method is a little primitive
330
# right now - RBC 20060928
332
match_revno = tuple((int(number) for number in revno_spec.split('.')))
333
except ValueError, e:
334
raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
339
# the user has override the branch to look in.
340
# we need to refresh the revision_history map and
342
from bzrlib.branch import Branch
343
branch = Branch.open(branch_spec)
349
revision_id_to_revno = branch.get_revision_id_to_revno_map()
350
revisions = [revision_id for revision_id, revno
351
in revision_id_to_revno.iteritems()
352
if revno == match_revno]
355
if len(revisions) != 1:
356
return branch, None, None
358
# there is no traditional 'revno' for dotted-decimal revnos.
359
# so for API compatability we return None.
360
return branch, None, revisions[0]
362
last_revno, last_revision_id = branch.last_revision_info()
364
# if get_rev_id supported negative revnos, there would not be a
365
# need for this special case.
366
if (-revno) >= last_revno:
369
revno = last_revno + revno + 1
371
revision_id = branch.get_rev_id(revno, revs_or_none)
372
except errors.NoSuchRevision:
373
raise errors.InvalidRevisionSpec(self.user_spec, branch)
374
return branch, revno, revision_id
376
def _as_revision_id(self, context_branch):
377
# We would have the revno here, but we don't really care
378
branch, revno, revision_id = self._lookup(context_branch, None)
381
def needs_branch(self):
382
return self.spec.find(':') == -1
384
def get_branch(self):
385
if self.spec.find(':') == -1:
388
return self.spec[self.spec.find(':')+1:]
391
RevisionSpec_int = RevisionSpec_revno
393
SPEC_TYPES.append(RevisionSpec_revno)
396
class RevisionSpec_revid(RevisionSpec):
397
"""Selects a revision using the revision id."""
399
help_txt = """Selects a revision using the revision id.
401
Supply a specific revision id, that can be used to specify any
402
revision id in the ancestry of the branch.
403
Including merges, and pending merges.
406
revid:aaaa@bbbb-123456789 -> Select revision 'aaaa@bbbb-123456789'
411
def _match_on(self, branch, revs):
412
# self.spec comes straight from parsing the command line arguments,
413
# so we expect it to be a Unicode string. Switch it to the internal
415
revision_id = osutils.safe_revision_id(self.spec, warn=False)
416
return RevisionInfo.from_revision_id(branch, revision_id, revs)
418
def _as_revision_id(self, context_branch):
419
return osutils.safe_revision_id(self.spec, warn=False)
421
SPEC_TYPES.append(RevisionSpec_revid)
424
class RevisionSpec_last(RevisionSpec):
425
"""Selects the nth revision from the end."""
427
help_txt = """Selects the nth revision from the end.
429
Supply a positive number to get the nth revision from the end.
430
This is the same as supplying negative numbers to the 'revno:' spec.
433
last:1 -> return the last revision
434
last:3 -> return the revision 2 before the end.
439
def _match_on(self, branch, revs):
440
revno, revision_id = self._revno_and_revision_id(branch, revs)
441
return RevisionInfo(branch, revno, revision_id)
443
def _revno_and_revision_id(self, context_branch, revs_or_none):
444
last_revno, last_revision_id = context_branch.last_revision_info()
448
raise errors.NoCommits(context_branch)
449
return last_revno, last_revision_id
452
offset = int(self.spec)
453
except ValueError, e:
454
raise errors.InvalidRevisionSpec(self.user_spec, context_branch, e)
457
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
458
'you must supply a positive value')
460
revno = last_revno - offset + 1
462
revision_id = context_branch.get_rev_id(revno, revs_or_none)
463
except errors.NoSuchRevision:
464
raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
465
return revno, revision_id
467
def _as_revision_id(self, context_branch):
468
# We compute the revno as part of the process, but we don't really care
470
revno, revision_id = self._revno_and_revision_id(context_branch, None)
473
SPEC_TYPES.append(RevisionSpec_last)
476
class RevisionSpec_before(RevisionSpec):
477
"""Selects the parent of the revision specified."""
479
help_txt = """Selects the parent of the revision specified.
481
Supply any revision spec to return the parent of that revision.
482
It is an error to request the parent of the null revision (before:0).
483
This is mostly useful when inspecting revisions that are not in the
484
revision history of a branch.
488
before:1913 -> Return the parent of revno 1913 (revno 1912)
489
before:revid:aaaa@bbbb-1234567890 -> return the parent of revision
491
bzr diff -r before:revid:aaaa..revid:aaaa
492
-> Find the changes between revision 'aaaa' and its parent.
493
(what changes did 'aaaa' introduce)
498
def _match_on(self, branch, revs):
499
r = RevisionSpec.from_string(self.spec)._match_on(branch, revs)
501
raise errors.InvalidRevisionSpec(self.user_spec, branch,
502
'cannot go before the null: revision')
504
# We need to use the repository history here
505
rev = branch.repository.get_revision(r.rev_id)
506
if not rev.parent_ids:
508
revision_id = revision.NULL_REVISION
510
revision_id = rev.parent_ids[0]
512
revno = revs.index(revision_id) + 1
518
revision_id = branch.get_rev_id(revno, revs)
519
except errors.NoSuchRevision:
520
raise errors.InvalidRevisionSpec(self.user_spec,
522
return RevisionInfo(branch, revno, revision_id)
524
def _as_revision_id(self, context_branch):
525
base_revspec = RevisionSpec.from_string(self.spec)
526
base_revision_id = base_revspec.as_revision_id(context_branch)
527
if base_revision_id == revision.NULL_REVISION:
528
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
529
'cannot go before the null: revision')
530
context_repo = context_branch.repository
531
context_repo.lock_read()
533
parent_map = context_repo.get_parent_map([base_revision_id])
535
context_repo.unlock()
536
if base_revision_id not in parent_map:
537
# Ghost, or unknown revision id
538
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
539
'cannot find the matching revision')
540
parents = parent_map[base_revision_id]
542
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
543
'No parents for revision.')
546
SPEC_TYPES.append(RevisionSpec_before)
549
class RevisionSpec_tag(RevisionSpec):
550
"""Select a revision identified by tag name"""
552
help_txt = """Selects a revision identified by a tag name.
554
Tags are stored in the branch and created by the 'tag' command.
559
def _match_on(self, branch, revs):
560
# Can raise tags not supported, NoSuchTag, etc
561
return RevisionInfo.from_revision_id(branch,
562
branch.tags.lookup_tag(self.spec),
565
def _as_revision_id(self, context_branch):
566
return context_branch.tags.lookup_tag(self.spec)
568
SPEC_TYPES.append(RevisionSpec_tag)
571
class _RevListToTimestamps(object):
572
"""This takes a list of revisions, and allows you to bisect by date"""
574
__slots__ = ['revs', 'branch']
576
def __init__(self, revs, branch):
580
def __getitem__(self, index):
581
"""Get the date of the index'd item"""
582
r = self.branch.repository.get_revision(self.revs[index])
583
# TODO: Handle timezone.
584
return datetime.datetime.fromtimestamp(r.timestamp)
587
return len(self.revs)
590
class RevisionSpec_date(RevisionSpec):
591
"""Selects a revision on the basis of a datestamp."""
593
help_txt = """Selects a revision on the basis of a datestamp.
595
Supply a datestamp to select the first revision that matches the date.
596
Date can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
597
Matches the first entry after a given date (either at midnight or
598
at a specified time).
600
One way to display all the changes since yesterday would be::
602
bzr log -r date:yesterday..
606
date:yesterday -> select the first revision since yesterday
607
date:2006-08-14,17:10:14 -> select the first revision after
608
August 14th, 2006 at 5:10pm.
611
_date_re = re.compile(
612
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
614
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
617
def _match_on(self, branch, revs):
618
"""Spec for date revisions:
620
value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
621
matches the first entry after a given date (either at midnight or
622
at a specified time).
624
# XXX: This doesn't actually work
625
# So the proper way of saying 'give me all entries for today' is:
626
# -r date:yesterday..date:today
627
today = datetime.datetime.fromordinal(datetime.date.today().toordinal())
628
if self.spec.lower() == 'yesterday':
629
dt = today - datetime.timedelta(days=1)
630
elif self.spec.lower() == 'today':
632
elif self.spec.lower() == 'tomorrow':
633
dt = today + datetime.timedelta(days=1)
635
m = self._date_re.match(self.spec)
636
if not m or (not m.group('date') and not m.group('time')):
637
raise errors.InvalidRevisionSpec(self.user_spec,
638
branch, 'invalid date')
642
year = int(m.group('year'))
643
month = int(m.group('month'))
644
day = int(m.group('day'))
651
hour = int(m.group('hour'))
652
minute = int(m.group('minute'))
653
if m.group('second'):
654
second = int(m.group('second'))
658
hour, minute, second = 0,0,0
660
raise errors.InvalidRevisionSpec(self.user_spec,
661
branch, 'invalid date')
663
dt = datetime.datetime(year=year, month=month, day=day,
664
hour=hour, minute=minute, second=second)
667
rev = bisect.bisect(_RevListToTimestamps(revs, branch), dt)
671
raise errors.InvalidRevisionSpec(self.user_spec, branch)
673
return RevisionInfo(branch, rev + 1)
675
SPEC_TYPES.append(RevisionSpec_date)
678
class RevisionSpec_ancestor(RevisionSpec):
679
"""Selects a common ancestor with a second branch."""
681
help_txt = """Selects a common ancestor with a second branch.
683
Supply the path to a branch to select the common ancestor.
685
The common ancestor is the last revision that existed in both
686
branches. Usually this is the branch point, but it could also be
687
a revision that was merged.
689
This is frequently used with 'diff' to return all of the changes
690
that your branch introduces, while excluding the changes that you
691
have not merged from the remote branch.
695
ancestor:/path/to/branch
696
$ bzr diff -r ancestor:../../mainline/branch
700
def _match_on(self, branch, revs):
701
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
702
return self._find_revision_info(branch, self.spec)
704
def _as_revision_id(self, context_branch):
705
return self._find_revision_id(context_branch, self.spec)
708
def _find_revision_info(branch, other_location):
709
revision_id = RevisionSpec_ancestor._find_revision_id(branch,
712
revno = branch.revision_id_to_revno(revision_id)
713
except errors.NoSuchRevision:
715
return RevisionInfo(branch, revno, revision_id)
718
def _find_revision_id(branch, other_location):
719
from bzrlib.branch import Branch
723
revision_a = revision.ensure_null(branch.last_revision())
724
if revision_a == revision.NULL_REVISION:
725
raise errors.NoCommits(branch)
726
other_branch = Branch.open(other_location)
727
other_branch.lock_read()
729
revision_b = revision.ensure_null(other_branch.last_revision())
730
if revision_b == revision.NULL_REVISION:
731
raise errors.NoCommits(other_branch)
732
graph = branch.repository.get_graph(other_branch.repository)
733
rev_id = graph.find_unique_lca(revision_a, revision_b)
735
other_branch.unlock()
736
if rev_id == revision.NULL_REVISION:
737
raise errors.NoCommonAncestor(revision_a, revision_b)
743
SPEC_TYPES.append(RevisionSpec_ancestor)
746
class RevisionSpec_branch(RevisionSpec):
747
"""Selects the last revision of a specified branch."""
749
help_txt = """Selects the last revision of a specified branch.
751
Supply the path to a branch to select its last revision.
755
branch:/path/to/branch
759
def _match_on(self, branch, revs):
760
from bzrlib.branch import Branch
761
other_branch = Branch.open(self.spec)
762
revision_b = other_branch.last_revision()
763
if revision_b in (None, revision.NULL_REVISION):
764
raise errors.NoCommits(other_branch)
765
# pull in the remote revisions so we can diff
766
branch.fetch(other_branch, revision_b)
768
revno = branch.revision_id_to_revno(revision_b)
769
except errors.NoSuchRevision:
771
return RevisionInfo(branch, revno, revision_b)
773
def _as_revision_id(self, context_branch):
774
from bzrlib.branch import Branch
775
other_branch = Branch.open(self.spec)
776
last_revision = other_branch.last_revision()
777
last_revision = revision.ensure_null(last_revision)
778
context_branch.fetch(other_branch, last_revision)
779
if last_revision == revision.NULL_REVISION:
780
raise errors.NoCommits(other_branch)
783
SPEC_TYPES.append(RevisionSpec_branch)
786
class RevisionSpec_submit(RevisionSpec_ancestor):
787
"""Selects a common ancestor with a submit branch."""
789
help_txt = """Selects a common ancestor with the submit branch.
791
Diffing against this shows all the changes that were made in this branch,
792
and is a good predictor of what merge will do. The submit branch is
793
used by the bundle and merge directive commands. If no submit branch
794
is specified, the parent branch is used instead.
796
The common ancestor is the last revision that existed in both
797
branches. Usually this is the branch point, but it could also be
798
a revision that was merged.
802
$ bzr diff -r submit:
807
def _get_submit_location(self, branch):
808
submit_location = branch.get_submit_branch()
809
location_type = 'submit branch'
810
if submit_location is None:
811
submit_location = branch.get_parent()
812
location_type = 'parent branch'
813
if submit_location is None:
814
raise errors.NoSubmitBranch(branch)
815
trace.note('Using %s %s', location_type, submit_location)
816
return submit_location
818
def _match_on(self, branch, revs):
819
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
820
return self._find_revision_info(branch,
821
self._get_submit_location(branch))
823
def _as_revision_id(self, context_branch):
824
return self._find_revision_id(context_branch,
825
self._get_submit_location(context_branch))
828
SPEC_TYPES.append(RevisionSpec_submit)