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
35
class RevisionInfo(object):
36
"""The results of applying a revision specification to a branch."""
38
help_txt = """The results of applying a revision specification to a branch.
40
An instance has two useful attributes: revno, and rev_id.
42
They can also be accessed as spec[0] and spec[1] respectively,
43
so that you can write code like:
44
revno, rev_id = RevisionSpec(branch, spec)
45
although this is probably going to be deprecated later.
47
This class exists mostly to be the return value of a RevisionSpec,
48
so that you can access the member you're interested in (number or id)
49
or treat the result as a tuple.
52
def __init__(self, branch, revno, rev_id=_marker):
56
# allow caller to be lazy
57
if self.revno is None:
60
self.rev_id = branch.get_rev_id(self.revno)
64
def __nonzero__(self):
65
# first the easy ones...
66
if self.rev_id is None:
68
if self.revno is not None:
70
# TODO: otherwise, it should depend on how I was built -
71
# if it's in_history(branch), then check revision_history(),
72
# if it's in_store(branch), do the check below
73
return self.branch.repository.has_revision(self.rev_id)
78
def __getitem__(self, index):
79
if index == 0: return self.revno
80
if index == 1: return self.rev_id
81
raise IndexError(index)
84
return self.branch.repository.get_revision(self.rev_id)
86
def __eq__(self, other):
87
if type(other) not in (tuple, list, type(self)):
89
if type(other) is type(self) and self.branch is not other.branch:
91
return tuple(self) == tuple(other)
94
return '<bzrlib.revisionspec.RevisionInfo object %s, %s for %r>' % (
95
self.revno, self.rev_id, self.branch)
98
def from_revision_id(branch, revision_id, revs):
99
"""Construct a RevisionInfo given just the id.
101
Use this if you don't know or care what the revno is.
103
if revision_id == revision.NULL_REVISION:
104
return RevisionInfo(branch, 0, revision_id)
106
revno = revs.index(revision_id) + 1
109
return RevisionInfo(branch, revno, revision_id)
112
# classes in this list should have a "prefix" attribute, against which
113
# string specs are matched
118
class RevisionSpec(object):
119
"""A parsed revision specification."""
121
help_txt = """A parsed revision specification.
123
A revision specification can be an integer, in which case it is
124
assumed to be a revno (though this will translate negative values
125
into positive ones); or it can be a string, in which case it is
126
parsed for something like 'date:' or 'revid:' etc.
128
Revision specs are an UI element, and they have been moved out
129
of the branch class to leave "back-end" classes unaware of such
130
details. Code that gets a revno or rev_id from other code should
131
not be using revision specs - revnos and revision ids are the
132
accepted ways to refer to revisions internally.
134
(Equivalent to the old Branch method get_revision_info())
138
wants_revision_history = True
141
def from_string(spec):
142
"""Parse a revision spec string into a RevisionSpec object.
144
:param spec: A string specified by the user
145
:return: A RevisionSpec object that understands how to parse the
148
if not isinstance(spec, (type(None), basestring)):
149
raise TypeError('error')
152
return RevisionSpec(None, _internal=True)
153
for spectype in SPEC_TYPES:
154
if spec.startswith(spectype.prefix):
155
trace.mutter('Returning RevisionSpec %s for %s',
156
spectype.__name__, spec)
157
return spectype(spec, _internal=True)
159
# RevisionSpec_revno is special cased, because it is the only
160
# one that directly handles plain integers
161
# TODO: This should not be special cased rather it should be
162
# a method invocation on spectype.canparse()
164
if _revno_regex is None:
165
_revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
166
if _revno_regex.match(spec) is not None:
167
return RevisionSpec_revno(spec, _internal=True)
169
raise errors.NoSuchRevisionSpec(spec)
171
def __init__(self, spec, _internal=False):
172
"""Create a RevisionSpec referring to the Null revision.
174
:param spec: The original spec supplied by the user
175
:param _internal: Used to ensure that RevisionSpec is not being
176
called directly. Only from RevisionSpec.from_string()
179
# XXX: Update this after 0.10 is released
180
symbol_versioning.warn('Creating a RevisionSpec directly has'
181
' been deprecated in version 0.11. Use'
182
' RevisionSpec.from_string()'
184
DeprecationWarning, stacklevel=2)
185
self.user_spec = spec
186
if self.prefix and spec.startswith(self.prefix):
187
spec = spec[len(self.prefix):]
190
def _match_on(self, branch, revs):
191
trace.mutter('Returning RevisionSpec._match_on: None')
192
return RevisionInfo(branch, None, None)
194
def _match_on_and_check(self, branch, revs):
195
info = self._match_on(branch, revs)
198
elif info == (None, None):
199
# special case - nothing supplied
202
raise errors.InvalidRevisionSpec(self.user_spec, branch)
204
raise errors.InvalidRevisionSpec(self.spec, branch)
206
def in_history(self, branch):
208
if self.wants_revision_history:
209
revs = branch.revision_history()
213
# this should never trigger.
214
# TODO: make it a deprecated code path. RBC 20060928
216
return self._match_on_and_check(branch, revs)
218
# FIXME: in_history is somewhat broken,
219
# it will return non-history revisions in many
220
# circumstances. The expected facility is that
221
# in_history only returns revision-history revs,
222
# in_store returns any rev. RBC 20051010
223
# aliases for now, when we fix the core logic, then they
224
# will do what you expect.
225
in_store = in_history
228
def as_revision_id(self, context_branch):
229
"""Return just the revision_id for this revisions spec.
231
Some revision specs require a context_branch to be able to determine
232
their value. Not all specs will make use of it.
234
return self._as_revision_id(context_branch)
236
def _as_revision_id(self, context_branch):
237
"""Implementation of as_revision_id()
239
Classes should override this function to provide appropriate
240
functionality. The default is to just call '.in_history().rev_id'
242
return self.in_history(context_branch).rev_id
244
def as_tree(self, context_branch):
245
"""Return the tree object for this revisions spec.
247
Some revision specs require a context_branch to be able to determine
248
the revision id and access the repository. Not all specs will make
251
return self._as_tree(context_branch)
253
def _as_tree(self, context_branch):
254
"""Implementation of as_tree().
256
Classes should override this function to provide appropriate
257
functionality. The default is to just call '.as_revision_id()'
258
and get the revision tree from context_branch's repository.
260
revision_id = self.as_revision_id(context_branch)
261
return context_branch.repository.revision_tree(revision_id)
264
# this is mostly for helping with testing
265
return '<%s %s>' % (self.__class__.__name__,
268
def needs_branch(self):
269
"""Whether this revision spec needs a branch.
271
Set this to False the branch argument of _match_on is not used.
275
def get_branch(self):
276
"""When the revision specifier contains a branch location, return it.
278
Otherwise, return None.
285
class RevisionSpec_revno(RevisionSpec):
286
"""Selects a revision using a number."""
288
help_txt = """Selects a revision using a number.
290
Use an integer to specify a revision in the history of the branch.
291
Optionally a branch can be specified. The 'revno:' prefix is optional.
292
A negative number will count from the end of the branch (-1 is the
293
last revision, -2 the previous one). If the negative number is larger
294
than the branch's history, the first revision is returned.
297
revno:1 -> return the first revision of this branch
298
revno:3:/path/to/branch -> return the 3rd revision of
299
the branch '/path/to/branch'
300
revno:-1 -> The last revision in a branch.
301
-2:http://other/branch -> The second to last revision in the
303
-1000000 -> Most likely the first revision, unless
304
your history is very long.
307
wants_revision_history = False
309
def _match_on(self, branch, revs):
310
"""Lookup a revision by revision number"""
311
branch, revno, revision_id = self._lookup(branch, revs)
312
return RevisionInfo(branch, revno, revision_id)
314
def _lookup(self, branch, revs_or_none):
315
loc = self.spec.find(':')
317
revno_spec = self.spec
320
revno_spec = self.spec[:loc]
321
branch_spec = self.spec[loc+1:]
325
raise errors.InvalidRevisionSpec(self.user_spec,
326
branch, 'cannot have an empty revno and no branch')
330
revno = int(revno_spec)
333
# dotted decimal. This arguably should not be here
334
# but the from_string method is a little primitive
335
# right now - RBC 20060928
337
match_revno = tuple((int(number) for number in revno_spec.split('.')))
338
except ValueError, e:
339
raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
344
# the user has override the branch to look in.
345
# we need to refresh the revision_history map and
347
from bzrlib.branch import Branch
348
branch = Branch.open(branch_spec)
354
revision_id_to_revno = branch.get_revision_id_to_revno_map()
355
revisions = [revision_id for revision_id, revno
356
in revision_id_to_revno.iteritems()
357
if revno == match_revno]
360
if len(revisions) != 1:
361
return branch, None, None
363
# there is no traditional 'revno' for dotted-decimal revnos.
364
# so for API compatability we return None.
365
return branch, None, revisions[0]
367
last_revno, last_revision_id = branch.last_revision_info()
369
# if get_rev_id supported negative revnos, there would not be a
370
# need for this special case.
371
if (-revno) >= last_revno:
374
revno = last_revno + revno + 1
376
revision_id = branch.get_rev_id(revno, revs_or_none)
377
except errors.NoSuchRevision:
378
raise errors.InvalidRevisionSpec(self.user_spec, branch)
379
return branch, revno, revision_id
381
def _as_revision_id(self, context_branch):
382
# We would have the revno here, but we don't really care
383
branch, revno, revision_id = self._lookup(context_branch, None)
386
def needs_branch(self):
387
return self.spec.find(':') == -1
389
def get_branch(self):
390
if self.spec.find(':') == -1:
393
return self.spec[self.spec.find(':')+1:]
396
RevisionSpec_int = RevisionSpec_revno
398
SPEC_TYPES.append(RevisionSpec_revno)
401
class RevisionSpec_revid(RevisionSpec):
402
"""Selects a revision using the revision id."""
404
help_txt = """Selects a revision using the revision id.
406
Supply a specific revision id, that can be used to specify any
407
revision id in the ancestry of the branch.
408
Including merges, and pending merges.
411
revid:aaaa@bbbb-123456789 -> Select revision 'aaaa@bbbb-123456789'
416
def _match_on(self, branch, revs):
417
# self.spec comes straight from parsing the command line arguments,
418
# so we expect it to be a Unicode string. Switch it to the internal
420
revision_id = osutils.safe_revision_id(self.spec, warn=False)
421
return RevisionInfo.from_revision_id(branch, revision_id, revs)
423
def _as_revision_id(self, context_branch):
424
return osutils.safe_revision_id(self.spec, warn=False)
426
SPEC_TYPES.append(RevisionSpec_revid)
429
class RevisionSpec_last(RevisionSpec):
430
"""Selects the nth revision from the end."""
432
help_txt = """Selects the nth revision from the end.
434
Supply a positive number to get the nth revision from the end.
435
This is the same as supplying negative numbers to the 'revno:' spec.
438
last:1 -> return the last revision
439
last:3 -> return the revision 2 before the end.
444
def _match_on(self, branch, revs):
445
revno, revision_id = self._revno_and_revision_id(branch, revs)
446
return RevisionInfo(branch, revno, revision_id)
448
def _revno_and_revision_id(self, context_branch, revs_or_none):
449
last_revno, last_revision_id = context_branch.last_revision_info()
453
raise errors.NoCommits(context_branch)
454
return last_revno, last_revision_id
457
offset = int(self.spec)
458
except ValueError, e:
459
raise errors.InvalidRevisionSpec(self.user_spec, context_branch, e)
462
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
463
'you must supply a positive value')
465
revno = last_revno - offset + 1
467
revision_id = context_branch.get_rev_id(revno, revs_or_none)
468
except errors.NoSuchRevision:
469
raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
470
return revno, revision_id
472
def _as_revision_id(self, context_branch):
473
# We compute the revno as part of the process, but we don't really care
475
revno, revision_id = self._revno_and_revision_id(context_branch, None)
478
SPEC_TYPES.append(RevisionSpec_last)
481
class RevisionSpec_before(RevisionSpec):
482
"""Selects the parent of the revision specified."""
484
help_txt = """Selects the parent of the revision specified.
486
Supply any revision spec to return the parent of that revision. This is
487
mostly useful when inspecting revisions that are not in the revision history
490
It is an error to request the parent of the null revision (before:0).
494
before:1913 -> Return the parent of revno 1913 (revno 1912)
495
before:revid:aaaa@bbbb-1234567890 -> return the parent of revision
497
bzr diff -r before:1913..1913
498
-> Find the changes between revision 1913 and its parent (1912).
499
(What changes did revision 1913 introduce).
500
This is equivalent to: bzr diff -c 1913
505
def _match_on(self, branch, revs):
506
r = RevisionSpec.from_string(self.spec)._match_on(branch, revs)
508
raise errors.InvalidRevisionSpec(self.user_spec, branch,
509
'cannot go before the null: revision')
511
# We need to use the repository history here
512
rev = branch.repository.get_revision(r.rev_id)
513
if not rev.parent_ids:
515
revision_id = revision.NULL_REVISION
517
revision_id = rev.parent_ids[0]
519
revno = revs.index(revision_id) + 1
525
revision_id = branch.get_rev_id(revno, revs)
526
except errors.NoSuchRevision:
527
raise errors.InvalidRevisionSpec(self.user_spec,
529
return RevisionInfo(branch, revno, revision_id)
531
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)
534
if base_revision_id == revision.NULL_REVISION:
535
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
536
'cannot go before the null: revision')
537
context_repo = context_branch.repository
538
context_repo.lock_read()
540
parent_map = context_repo.get_parent_map([base_revision_id])
542
context_repo.unlock()
543
if base_revision_id not in parent_map:
544
# Ghost, or unknown revision id
545
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
546
'cannot find the matching revision')
547
parents = parent_map[base_revision_id]
549
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
550
'No parents for revision.')
553
SPEC_TYPES.append(RevisionSpec_before)
556
class RevisionSpec_tag(RevisionSpec):
557
"""Select a revision identified by tag name"""
559
help_txt = """Selects a revision identified by a tag name.
561
Tags are stored in the branch and created by the 'tag' command.
566
def _match_on(self, branch, revs):
567
# Can raise tags not supported, NoSuchTag, etc
568
return RevisionInfo.from_revision_id(branch,
569
branch.tags.lookup_tag(self.spec),
572
def _as_revision_id(self, context_branch):
573
return context_branch.tags.lookup_tag(self.spec)
575
SPEC_TYPES.append(RevisionSpec_tag)
578
class _RevListToTimestamps(object):
579
"""This takes a list of revisions, and allows you to bisect by date"""
581
__slots__ = ['revs', 'branch']
583
def __init__(self, revs, branch):
587
def __getitem__(self, index):
588
"""Get the date of the index'd item"""
589
r = self.branch.repository.get_revision(self.revs[index])
590
# TODO: Handle timezone.
591
return datetime.datetime.fromtimestamp(r.timestamp)
594
return len(self.revs)
597
class RevisionSpec_date(RevisionSpec):
598
"""Selects a revision on the basis of a datestamp."""
600
help_txt = """Selects a revision on the basis of a datestamp.
602
Supply a datestamp to select the first revision that matches the date.
603
Date can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
604
Matches the first entry after a given date (either at midnight or
605
at a specified time).
607
One way to display all the changes since yesterday would be::
609
bzr log -r date:yesterday..
613
date:yesterday -> select the first revision since yesterday
614
date:2006-08-14,17:10:14 -> select the first revision after
615
August 14th, 2006 at 5:10pm.
618
_date_re = re.compile(
619
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
621
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
624
def _match_on(self, branch, revs):
625
"""Spec for date revisions:
627
value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
628
matches the first entry after a given date (either at midnight or
629
at a specified time).
631
# XXX: This doesn't actually work
632
# So the proper way of saying 'give me all entries for today' is:
633
# -r date:yesterday..date:today
634
today = datetime.datetime.fromordinal(datetime.date.today().toordinal())
635
if self.spec.lower() == 'yesterday':
636
dt = today - datetime.timedelta(days=1)
637
elif self.spec.lower() == 'today':
639
elif self.spec.lower() == 'tomorrow':
640
dt = today + datetime.timedelta(days=1)
642
m = self._date_re.match(self.spec)
643
if not m or (not m.group('date') and not m.group('time')):
644
raise errors.InvalidRevisionSpec(self.user_spec,
645
branch, 'invalid date')
649
year = int(m.group('year'))
650
month = int(m.group('month'))
651
day = int(m.group('day'))
658
hour = int(m.group('hour'))
659
minute = int(m.group('minute'))
660
if m.group('second'):
661
second = int(m.group('second'))
665
hour, minute, second = 0,0,0
667
raise errors.InvalidRevisionSpec(self.user_spec,
668
branch, 'invalid date')
670
dt = datetime.datetime(year=year, month=month, day=day,
671
hour=hour, minute=minute, second=second)
674
rev = bisect.bisect(_RevListToTimestamps(revs, branch), dt)
678
raise errors.InvalidRevisionSpec(self.user_spec, branch)
680
return RevisionInfo(branch, rev + 1)
682
SPEC_TYPES.append(RevisionSpec_date)
685
class RevisionSpec_ancestor(RevisionSpec):
686
"""Selects a common ancestor with a second branch."""
688
help_txt = """Selects a common ancestor with a second branch.
690
Supply the path to a branch to select the common ancestor.
692
The common ancestor is the last revision that existed in both
693
branches. Usually this is the branch point, but it could also be
694
a revision that was merged.
696
This is frequently used with 'diff' to return all of the changes
697
that your branch introduces, while excluding the changes that you
698
have not merged from the remote branch.
702
ancestor:/path/to/branch
703
$ bzr diff -r ancestor:../../mainline/branch
707
def _match_on(self, branch, revs):
708
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
709
return self._find_revision_info(branch, self.spec)
711
def _as_revision_id(self, context_branch):
712
return self._find_revision_id(context_branch, self.spec)
715
def _find_revision_info(branch, other_location):
716
revision_id = RevisionSpec_ancestor._find_revision_id(branch,
719
revno = branch.revision_id_to_revno(revision_id)
720
except errors.NoSuchRevision:
722
return RevisionInfo(branch, revno, revision_id)
725
def _find_revision_id(branch, other_location):
726
from bzrlib.branch import Branch
730
revision_a = revision.ensure_null(branch.last_revision())
731
if revision_a == revision.NULL_REVISION:
732
raise errors.NoCommits(branch)
733
other_branch = Branch.open(other_location)
734
other_branch.lock_read()
736
revision_b = revision.ensure_null(other_branch.last_revision())
737
if revision_b == revision.NULL_REVISION:
738
raise errors.NoCommits(other_branch)
739
graph = branch.repository.get_graph(other_branch.repository)
740
rev_id = graph.find_unique_lca(revision_a, revision_b)
742
other_branch.unlock()
743
if rev_id == revision.NULL_REVISION:
744
raise errors.NoCommonAncestor(revision_a, revision_b)
750
SPEC_TYPES.append(RevisionSpec_ancestor)
753
class RevisionSpec_branch(RevisionSpec):
754
"""Selects the last revision of a specified branch."""
756
help_txt = """Selects the last revision of a specified branch.
758
Supply the path to a branch to select its last revision.
762
branch:/path/to/branch
766
def _match_on(self, branch, revs):
767
from bzrlib.branch import Branch
768
other_branch = Branch.open(self.spec)
769
revision_b = other_branch.last_revision()
770
if revision_b in (None, revision.NULL_REVISION):
771
raise errors.NoCommits(other_branch)
772
# pull in the remote revisions so we can diff
773
branch.fetch(other_branch, revision_b)
775
revno = branch.revision_id_to_revno(revision_b)
776
except errors.NoSuchRevision:
778
return RevisionInfo(branch, revno, revision_b)
780
def _as_revision_id(self, context_branch):
781
from bzrlib.branch import Branch
782
other_branch = Branch.open(self.spec)
783
last_revision = other_branch.last_revision()
784
last_revision = revision.ensure_null(last_revision)
785
context_branch.fetch(other_branch, last_revision)
786
if last_revision == revision.NULL_REVISION:
787
raise errors.NoCommits(other_branch)
790
def _as_tree(self, context_branch):
791
from bzrlib.branch import Branch
792
other_branch = Branch.open(self.spec)
793
last_revision = other_branch.last_revision()
794
last_revision = revision.ensure_null(last_revision)
795
if last_revision == revision.NULL_REVISION:
796
raise errors.NoCommits(other_branch)
797
return other_branch.repository.revision_tree(last_revision)
799
SPEC_TYPES.append(RevisionSpec_branch)
802
class RevisionSpec_submit(RevisionSpec_ancestor):
803
"""Selects a common ancestor with a submit branch."""
805
help_txt = """Selects a common ancestor with the submit branch.
807
Diffing against this shows all the changes that were made in this branch,
808
and is a good predictor of what merge will do. The submit branch is
809
used by the bundle and merge directive commands. If no submit branch
810
is specified, the parent branch is used instead.
812
The common ancestor is the last revision that existed in both
813
branches. Usually this is the branch point, but it could also be
814
a revision that was merged.
818
$ bzr diff -r submit:
823
def _get_submit_location(self, branch):
824
submit_location = branch.get_submit_branch()
825
location_type = 'submit branch'
826
if submit_location is None:
827
submit_location = branch.get_parent()
828
location_type = 'parent branch'
829
if submit_location is None:
830
raise errors.NoSubmitBranch(branch)
831
trace.note('Using %s %s', location_type, submit_location)
832
return submit_location
834
def _match_on(self, branch, revs):
835
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
836
return self._find_revision_info(branch,
837
self._get_submit_location(branch))
839
def _as_revision_id(self, context_branch):
840
return self._find_revision_id(context_branch,
841
self._get_submit_location(context_branch))
844
SPEC_TYPES.append(RevisionSpec_submit)