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())
139
def __new__(cls, spec, _internal=False):
141
return object.__new__(cls, spec, _internal=_internal)
143
symbol_versioning.warn('Creating a RevisionSpec directly has'
144
' been deprecated in version 0.11. Use'
145
' RevisionSpec.from_string()'
147
DeprecationWarning, stacklevel=2)
148
return RevisionSpec.from_string(spec)
151
def from_string(spec):
152
"""Parse a revision spec string into a RevisionSpec object.
154
:param spec: A string specified by the user
155
:return: A RevisionSpec object that understands how to parse the
158
if not isinstance(spec, (type(None), basestring)):
159
raise TypeError('error')
162
return RevisionSpec(None, _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)
169
# RevisionSpec_revno is special cased, because it is the only
170
# one that directly handles plain integers
171
# TODO: This should not be special cased rather it should be
172
# a method invocation on spectype.canparse()
174
if _revno_regex is None:
175
_revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
176
if _revno_regex.match(spec) is not None:
177
return RevisionSpec_revno(spec, _internal=True)
179
raise errors.NoSuchRevisionSpec(spec)
181
def __init__(self, spec, _internal=False):
182
"""Create a RevisionSpec referring to the Null revision.
184
:param spec: The original spec supplied by the user
185
:param _internal: Used to ensure that RevisionSpec is not being
186
called directly. Only from RevisionSpec.from_string()
189
# XXX: Update this after 0.10 is released
190
symbol_versioning.warn('Creating a RevisionSpec directly has'
191
' been deprecated in version 0.11. Use'
192
' RevisionSpec.from_string()'
194
DeprecationWarning, stacklevel=2)
195
self.user_spec = spec
196
if self.prefix and spec.startswith(self.prefix):
197
spec = spec[len(self.prefix):]
200
def _match_on(self, branch, revs):
201
trace.mutter('Returning RevisionSpec._match_on: None')
202
return RevisionInfo(branch, None, None)
204
def _match_on_and_check(self, branch, revs):
205
info = self._match_on(branch, revs)
208
elif info == (None, None):
209
# special case - nothing supplied
212
raise errors.InvalidRevisionSpec(self.user_spec, branch)
214
raise errors.InvalidRevisionSpec(self.spec, branch)
216
def in_history(self, branch):
218
revs = branch.revision_history()
220
# this should never trigger.
221
# TODO: make it a deprecated code path. RBC 20060928
223
return self._match_on_and_check(branch, revs)
225
# FIXME: in_history is somewhat broken,
226
# it will return non-history revisions in many
227
# circumstances. The expected facility is that
228
# in_history only returns revision-history revs,
229
# in_store returns any rev. RBC 20051010
230
# aliases for now, when we fix the core logic, then they
231
# will do what you expect.
232
in_store = in_history
235
def as_revision_id(self, context_branch):
236
"""Return just the revision_id for this revisions spec.
238
Some revision specs require a context_branch to be able to determine
239
their value. Not all specs will make use of it.
241
return self._as_revision_id(context_branch)
243
def _as_revision_id(self, context_branch):
244
"""Implementation of as_revision_id()
246
Classes should override this function to provide appropriate
247
functionality. The default is to just call '.in_history().rev_id'
249
return self.in_history(context_branch).rev_id
252
# this is mostly for helping with testing
253
return '<%s %s>' % (self.__class__.__name__,
256
def needs_branch(self):
257
"""Whether this revision spec needs a branch.
259
Set this to False the branch argument of _match_on is not used.
263
def get_branch(self):
264
"""When the revision specifier contains a branch location, return it.
266
Otherwise, return None.
273
class RevisionSpec_revno(RevisionSpec):
274
"""Selects a revision using a number."""
276
help_txt = """Selects a revision using a number.
278
Use an integer to specify a revision in the history of the branch.
279
Optionally a branch can be specified. The 'revno:' prefix is optional.
280
A negative number will count from the end of the branch (-1 is the
281
last revision, -2 the previous one). If the negative number is larger
282
than the branch's history, the first revision is returned.
285
revno:1 -> return the first revision
286
revno:3:/path/to/branch -> return the 3rd revision of
287
the branch '/path/to/branch'
288
revno:-1 -> The last revision in a branch.
289
-2:http://other/branch -> The second to last revision in the
291
-1000000 -> Most likely the first revision, unless
292
your history is very long.
296
def _match_on(self, branch, revs):
297
"""Lookup a revision by revision number"""
298
branch, revno, revision_id = self._lookup(branch, revs)
299
return RevisionInfo(branch, revno, revision_id)
301
def _lookup(self, branch, revs_or_none):
302
loc = self.spec.find(':')
304
revno_spec = self.spec
307
revno_spec = self.spec[:loc]
308
branch_spec = self.spec[loc+1:]
312
raise errors.InvalidRevisionSpec(self.user_spec,
313
branch, 'cannot have an empty revno and no branch')
317
revno = int(revno_spec)
320
# dotted decimal. This arguably should not be here
321
# but the from_string method is a little primitive
322
# right now - RBC 20060928
324
match_revno = tuple((int(number) for number in revno_spec.split('.')))
325
except ValueError, e:
326
raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
331
# the user has override the branch to look in.
332
# we need to refresh the revision_history map and
334
from bzrlib.branch import Branch
335
branch = Branch.open(branch_spec)
341
revision_id_to_revno = branch.get_revision_id_to_revno_map()
342
revisions = [revision_id for revision_id, revno
343
in revision_id_to_revno.iteritems()
344
if revno == match_revno]
347
if len(revisions) != 1:
348
return branch, None, None
350
# there is no traditional 'revno' for dotted-decimal revnos.
351
# so for API compatability we return None.
352
return branch, None, revisions[0]
354
last_revno, last_revision_id = branch.last_revision_info()
356
# if get_rev_id supported negative revnos, there would not be a
357
# need for this special case.
358
if (-revno) >= last_revno:
361
revno = last_revno + revno + 1
363
revision_id = branch.get_rev_id(revno, revs_or_none)
364
except errors.NoSuchRevision:
365
raise errors.InvalidRevisionSpec(self.user_spec, branch)
366
return branch, revno, revision_id
368
def _as_revision_id(self, context_branch):
369
# We would have the revno here, but we don't really care
370
branch, revno, revision_id = self._lookup(context_branch, None)
373
def needs_branch(self):
374
return self.spec.find(':') == -1
376
def get_branch(self):
377
if self.spec.find(':') == -1:
380
return self.spec[self.spec.find(':')+1:]
383
RevisionSpec_int = RevisionSpec_revno
385
SPEC_TYPES.append(RevisionSpec_revno)
388
class RevisionSpec_revid(RevisionSpec):
389
"""Selects a revision using the revision id."""
391
help_txt = """Selects a revision using the revision id.
393
Supply a specific revision id, that can be used to specify any
394
revision id in the ancestry of the branch.
395
Including merges, and pending merges.
398
revid:aaaa@bbbb-123456789 -> Select revision 'aaaa@bbbb-123456789'
403
def _match_on(self, branch, revs):
404
# self.spec comes straight from parsing the command line arguments,
405
# so we expect it to be a Unicode string. Switch it to the internal
407
revision_id = osutils.safe_revision_id(self.spec, warn=False)
408
return RevisionInfo.from_revision_id(branch, revision_id, revs)
410
def _as_revision_id(self, context_branch):
411
return osutils.safe_revision_id(self.spec, warn=False)
413
SPEC_TYPES.append(RevisionSpec_revid)
416
class RevisionSpec_last(RevisionSpec):
417
"""Selects the nth revision from the end."""
419
help_txt = """Selects the nth revision from the end.
421
Supply a positive number to get the nth revision from the end.
422
This is the same as supplying negative numbers to the 'revno:' spec.
425
last:1 -> return the last revision
426
last:3 -> return the revision 2 before the end.
431
def _match_on(self, branch, revs):
432
revno, revision_id = self._revno_and_revision_id(branch, revs)
433
return RevisionInfo(branch, revno, revision_id)
435
def _revno_and_revision_id(self, context_branch, revs_or_none):
436
last_revno, last_revision_id = context_branch.last_revision_info()
440
raise errors.NoCommits(context_branch)
441
return last_revno, last_revision_id
444
offset = int(self.spec)
445
except ValueError, e:
446
raise errors.InvalidRevisionSpec(self.user_spec, context_branch, e)
449
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
450
'you must supply a positive value')
452
revno = last_revno - offset + 1
454
revision_id = context_branch.get_rev_id(revno, revs_or_none)
455
except errors.NoSuchRevision:
456
raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
457
return revno, revision_id
459
def _as_revision_id(self, context_branch):
460
# We compute the revno as part of the process, but we don't really care
462
revno, revision_id = self._revno_and_revision_id(context_branch, None)
465
SPEC_TYPES.append(RevisionSpec_last)
468
class RevisionSpec_before(RevisionSpec):
469
"""Selects the parent of the revision specified."""
471
help_txt = """Selects the parent of the revision specified.
473
Supply any revision spec to return the parent of that revision.
474
It is an error to request the parent of the null revision (before:0).
475
This is mostly useful when inspecting revisions that are not in the
476
revision history of a branch.
480
before:1913 -> Return the parent of revno 1913 (revno 1912)
481
before:revid:aaaa@bbbb-1234567890 -> return the parent of revision
483
bzr diff -r before:revid:aaaa..revid:aaaa
484
-> Find the changes between revision 'aaaa' and its parent.
485
(what changes did 'aaaa' introduce)
490
def _match_on(self, branch, revs):
491
r = RevisionSpec.from_string(self.spec)._match_on(branch, revs)
493
raise errors.InvalidRevisionSpec(self.user_spec, branch,
494
'cannot go before the null: revision')
496
# We need to use the repository history here
497
rev = branch.repository.get_revision(r.rev_id)
498
if not rev.parent_ids:
500
revision_id = revision.NULL_REVISION
502
revision_id = rev.parent_ids[0]
504
revno = revs.index(revision_id) + 1
510
revision_id = branch.get_rev_id(revno, revs)
511
except errors.NoSuchRevision:
512
raise errors.InvalidRevisionSpec(self.user_spec,
514
return RevisionInfo(branch, revno, revision_id)
516
def _as_revision_id(self, context_branch):
517
base_revspec = RevisionSpec.from_string(self.spec)
518
base_revision_id = base_revspec.as_revision_id(context_branch)
519
if base_revision_id == revision.NULL_REVISION:
520
raise errors.InvalidRevisionSpec(self.user_spec, branch,
521
'cannot go before the null: revision')
522
context_repo = context_branch.repository
523
context_repo.lock_read()
525
parent_map = context_repo.get_parent_map([base_revision_id])
527
context_repo.unlock()
528
if base_revision_id not in parent_map:
529
# Ghost, or unknown revision id
530
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
531
'cannot find the matching revision')
532
parents = parent_map[base_revision_id]
534
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
535
'No parents for revision.')
538
SPEC_TYPES.append(RevisionSpec_before)
541
class RevisionSpec_tag(RevisionSpec):
542
"""Select a revision identified by tag name"""
544
help_txt = """Selects a revision identified by a tag name.
546
Tags are stored in the branch and created by the 'tag' command.
551
def _match_on(self, branch, revs):
552
# Can raise tags not supported, NoSuchTag, etc
553
return RevisionInfo.from_revision_id(branch,
554
branch.tags.lookup_tag(self.spec),
557
def _as_revision_id(self, context_branch):
558
return context_branch.tags.lookup_tag(self.spec)
560
SPEC_TYPES.append(RevisionSpec_tag)
563
class _RevListToTimestamps(object):
564
"""This takes a list of revisions, and allows you to bisect by date"""
566
__slots__ = ['revs', 'branch']
568
def __init__(self, revs, branch):
572
def __getitem__(self, index):
573
"""Get the date of the index'd item"""
574
r = self.branch.repository.get_revision(self.revs[index])
575
# TODO: Handle timezone.
576
return datetime.datetime.fromtimestamp(r.timestamp)
579
return len(self.revs)
582
class RevisionSpec_date(RevisionSpec):
583
"""Selects a revision on the basis of a datestamp."""
585
help_txt = """Selects a revision on the basis of a datestamp.
587
Supply a datestamp to select the first revision that matches the date.
588
Date can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
589
Matches the first entry after a given date (either at midnight or
590
at a specified time).
592
One way to display all the changes since yesterday would be::
594
bzr log -r date:yesterday..-1
598
date:yesterday -> select the first revision since yesterday
599
date:2006-08-14,17:10:14 -> select the first revision after
600
August 14th, 2006 at 5:10pm.
603
_date_re = re.compile(
604
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
606
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
609
def _match_on(self, branch, revs):
610
"""Spec for date revisions:
612
value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
613
matches the first entry after a given date (either at midnight or
614
at a specified time).
616
# XXX: This doesn't actually work
617
# So the proper way of saying 'give me all entries for today' is:
618
# -r date:yesterday..date:today
619
today = datetime.datetime.fromordinal(datetime.date.today().toordinal())
620
if self.spec.lower() == 'yesterday':
621
dt = today - datetime.timedelta(days=1)
622
elif self.spec.lower() == 'today':
624
elif self.spec.lower() == 'tomorrow':
625
dt = today + datetime.timedelta(days=1)
627
m = self._date_re.match(self.spec)
628
if not m or (not m.group('date') and not m.group('time')):
629
raise errors.InvalidRevisionSpec(self.user_spec,
630
branch, 'invalid date')
634
year = int(m.group('year'))
635
month = int(m.group('month'))
636
day = int(m.group('day'))
643
hour = int(m.group('hour'))
644
minute = int(m.group('minute'))
645
if m.group('second'):
646
second = int(m.group('second'))
650
hour, minute, second = 0,0,0
652
raise errors.InvalidRevisionSpec(self.user_spec,
653
branch, 'invalid date')
655
dt = datetime.datetime(year=year, month=month, day=day,
656
hour=hour, minute=minute, second=second)
659
rev = bisect.bisect(_RevListToTimestamps(revs, branch), dt)
663
raise errors.InvalidRevisionSpec(self.user_spec, branch)
665
return RevisionInfo(branch, rev + 1)
667
SPEC_TYPES.append(RevisionSpec_date)
670
class RevisionSpec_ancestor(RevisionSpec):
671
"""Selects a common ancestor with a second branch."""
673
help_txt = """Selects a common ancestor with a second branch.
675
Supply the path to a branch to select the common ancestor.
677
The common ancestor is the last revision that existed in both
678
branches. Usually this is the branch point, but it could also be
679
a revision that was merged.
681
This is frequently used with 'diff' to return all of the changes
682
that your branch introduces, while excluding the changes that you
683
have not merged from the remote branch.
687
ancestor:/path/to/branch
688
$ bzr diff -r ancestor:../../mainline/branch
692
def _match_on(self, branch, revs):
693
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
694
return self._find_revision_info(branch, self.spec)
696
def _as_revision_id(self, context_branch):
697
return self._find_revision_id(context_branch, self.spec)
700
def _find_revision_info(branch, other_location):
701
revision_id = RevisionSpec_ancestor._find_revision_id(branch,
704
revno = branch.revision_id_to_revno(revision_id)
705
except errors.NoSuchRevision:
707
return RevisionInfo(branch, revno, revision_id)
710
def _find_revision_id(branch, other_location):
711
from bzrlib.branch import Branch
715
revision_a = revision.ensure_null(branch.last_revision())
716
if revision_a == revision.NULL_REVISION:
717
raise errors.NoCommits(branch)
718
other_branch = Branch.open(other_location)
719
other_branch.lock_read()
721
revision_b = revision.ensure_null(other_branch.last_revision())
722
if revision_b == revision.NULL_REVISION:
723
raise errors.NoCommits(other_branch)
724
graph = branch.repository.get_graph(other_branch.repository)
725
rev_id = graph.find_unique_lca(revision_a, revision_b)
727
other_branch.unlock()
728
if rev_id == revision.NULL_REVISION:
729
raise errors.NoCommonAncestor(revision_a, revision_b)
735
SPEC_TYPES.append(RevisionSpec_ancestor)
738
class RevisionSpec_branch(RevisionSpec):
739
"""Selects the last revision of a specified branch."""
741
help_txt = """Selects the last revision of a specified branch.
743
Supply the path to a branch to select its last revision.
747
branch:/path/to/branch
751
def _match_on(self, branch, revs):
752
from bzrlib.branch import Branch
753
other_branch = Branch.open(self.spec)
754
revision_b = other_branch.last_revision()
755
if revision_b in (None, revision.NULL_REVISION):
756
raise errors.NoCommits(other_branch)
757
# pull in the remote revisions so we can diff
758
branch.fetch(other_branch, revision_b)
760
revno = branch.revision_id_to_revno(revision_b)
761
except errors.NoSuchRevision:
763
return RevisionInfo(branch, revno, revision_b)
765
def _as_revision_id(self, context_branch):
766
from bzrlib.branch import Branch
767
other_branch = Branch.open(self.spec)
768
last_revision = other_branch.last_revision()
769
last_revision = revision.ensure_null(last_revision)
770
context_branch.fetch(other_branch, last_revision)
771
if last_revision == revision.NULL_REVISION:
772
raise errors.NoCommits(other_branch)
775
SPEC_TYPES.append(RevisionSpec_branch)
778
class RevisionSpec_submit(RevisionSpec_ancestor):
779
"""Selects a common ancestor with a submit branch."""
781
help_txt = """Selects a common ancestor with the submit branch.
783
Diffing against this shows all the changes that were made in this branch,
784
and is a good predictor of what merge will do. The submit branch is
785
used by the bundle and merge directive comands. If no submit branch
786
is specified, the parent branch is used instead.
788
The common ancestor is the last revision that existed in both
789
branches. Usually this is the branch point, but it could also be
790
a revision that was merged.
794
$ bzr diff -r submit:
799
def _get_submit_location(self, branch):
800
submit_location = branch.get_submit_branch()
801
location_type = 'submit branch'
802
if submit_location is None:
803
submit_location = branch.get_parent()
804
location_type = 'parent branch'
805
if submit_location is None:
806
raise errors.NoSubmitBranch(branch)
807
trace.note('Using %s %s', location_type, submit_location)
808
return submit_location
810
def _match_on(self, branch, revs):
811
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
812
return self._find_revision_info(branch,
813
self._get_submit_location(branch))
815
def _as_revision_id(self, context_branch):
816
return self._find_revision_id(context_branch,
817
self._get_submit_location(context_branch))
820
SPEC_TYPES.append(RevisionSpec_submit)