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)
66
def __nonzero__(self):
67
# first the easy ones...
68
if self.rev_id is None:
70
if self.revno is not None:
72
# TODO: otherwise, it should depend on how I was built -
73
# if it's in_history(branch), then check revision_history(),
74
# if it's in_store(branch), do the check below
75
return self.branch.repository.has_revision(self.rev_id)
80
def __getitem__(self, index):
81
if index == 0: return self.revno
82
if index == 1: return self.rev_id
83
raise IndexError(index)
86
return self.branch.repository.get_revision(self.rev_id)
88
def __eq__(self, other):
89
if type(other) not in (tuple, list, type(self)):
91
if type(other) is type(self) and self.branch is not other.branch:
93
return tuple(self) == tuple(other)
96
return '<bzrlib.revisionspec.RevisionInfo object %s, %s for %r>' % (
97
self.revno, self.rev_id, self.branch)
100
def from_revision_id(branch, revision_id, revs):
101
"""Construct a RevisionInfo given just the id.
103
Use this if you don't know or care what the revno is.
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)
164
assert isinstance(spec, basestring), \
165
"You should only supply strings not %s" % (type(spec),)
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, 0, None)
208
def _match_on_and_check(self, branch, revs):
209
info = self._match_on(branch, revs)
212
elif info == (0, None):
213
# special case - the empty tree
216
raise errors.InvalidRevisionSpec(self.user_spec, branch)
218
raise errors.InvalidRevisionSpec(self.spec, branch)
220
def in_history(self, branch):
222
revs = branch.revision_history()
224
# this should never trigger.
225
# TODO: make it a deprecated code path. RBC 20060928
227
return self._match_on_and_check(branch, revs)
229
# FIXME: in_history is somewhat broken,
230
# it will return non-history revisions in many
231
# circumstances. The expected facility is that
232
# in_history only returns revision-history revs,
233
# in_store returns any rev. RBC 20051010
234
# aliases for now, when we fix the core logic, then they
235
# will do what you expect.
236
in_store = in_history
240
# this is mostly for helping with testing
241
return '<%s %s>' % (self.__class__.__name__,
244
def needs_branch(self):
245
"""Whether this revision spec needs a branch.
247
Set this to False the branch argument of _match_on is not used.
251
def get_branch(self):
252
"""When the revision specifier contains a branch location, return it.
254
Otherwise, return None.
261
class RevisionSpec_revno(RevisionSpec):
262
"""Selects a revision using a number."""
264
help_txt = """Selects a revision using a number.
266
Use an integer to specify a revision in the history of the branch.
267
Optionally a branch can be specified. The 'revno:' prefix is optional.
268
A negative number will count from the end of the branch (-1 is the
269
last revision, -2 the previous one). If the negative number is larger
270
than the branch's history, the first revision is returned.
272
revno:1 -> return the first revision
273
revno:3:/path/to/branch -> return the 3rd revision of
274
the branch '/path/to/branch'
275
revno:-1 -> The last revision in a branch.
276
-2:http://other/branch -> The second to last revision in the
278
-1000000 -> Most likely the first revision, unless
279
your history is very long.
283
def _match_on(self, branch, revs):
284
"""Lookup a revision by revision number"""
285
loc = self.spec.find(':')
287
revno_spec = self.spec
290
revno_spec = self.spec[:loc]
291
branch_spec = self.spec[loc+1:]
295
raise errors.InvalidRevisionSpec(self.user_spec,
296
branch, 'cannot have an empty revno and no branch')
300
revno = int(revno_spec)
303
# dotted decimal. This arguably should not be here
304
# but the from_string method is a little primitive
305
# right now - RBC 20060928
307
match_revno = tuple((int(number) for number in revno_spec.split('.')))
308
except ValueError, e:
309
raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
314
# the user has override the branch to look in.
315
# we need to refresh the revision_history map and
317
from bzrlib.branch import Branch
318
branch = Branch.open(branch_spec)
319
# Need to use a new revision history
320
# because we are using a specific branch
321
revs = branch.revision_history()
326
revision_id_to_revno = branch.get_revision_id_to_revno_map()
327
revisions = [revision_id for revision_id, revno
328
in revision_id_to_revno.iteritems()
329
if revno == match_revno]
332
if len(revisions) != 1:
333
return RevisionInfo(branch, None, None)
335
# there is no traditional 'revno' for dotted-decimal revnos.
336
# so for API compatability we return None.
337
return RevisionInfo(branch, None, revisions[0])
340
# if get_rev_id supported negative revnos, there would not be a
341
# need for this special case.
342
if (-revno) >= len(revs):
345
revno = len(revs) + revno + 1
347
revision_id = branch.get_rev_id(revno, revs)
348
except errors.NoSuchRevision:
349
raise errors.InvalidRevisionSpec(self.user_spec, branch)
350
return RevisionInfo(branch, revno, revision_id)
352
def needs_branch(self):
353
return self.spec.find(':') == -1
355
def get_branch(self):
356
if self.spec.find(':') == -1:
359
return self.spec[self.spec.find(':')+1:]
362
RevisionSpec_int = RevisionSpec_revno
364
SPEC_TYPES.append(RevisionSpec_revno)
367
class RevisionSpec_revid(RevisionSpec):
368
"""Selects a revision using the revision id."""
370
help_txt = """Selects a revision using the revision id.
372
Supply a specific revision id, that can be used to specify any
373
revision id in the ancestry of the branch.
374
Including merges, and pending merges.
376
revid:aaaa@bbbb-123456789 -> Select revision 'aaaa@bbbb-123456789'
380
def _match_on(self, branch, revs):
381
# self.spec comes straight from parsing the command line arguments,
382
# so we expect it to be a Unicode string. Switch it to the internal
384
revision_id = osutils.safe_revision_id(self.spec, warn=False)
385
return RevisionInfo.from_revision_id(branch, revision_id, revs)
387
SPEC_TYPES.append(RevisionSpec_revid)
390
class RevisionSpec_last(RevisionSpec):
391
"""Selects the nth revision from the end."""
393
help_txt = """Selects the nth revision from the end.
395
Supply a positive number to get the nth revision from the end.
396
This is the same as supplying negative numbers to the 'revno:' spec.
398
last:1 -> return the last revision
399
last:3 -> return the revision 2 before the end.
404
def _match_on(self, branch, revs):
407
raise errors.NoCommits(branch)
408
return RevisionInfo(branch, len(revs), revs[-1])
411
offset = int(self.spec)
412
except ValueError, e:
413
raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
416
raise errors.InvalidRevisionSpec(self.user_spec, branch,
417
'you must supply a positive value')
418
revno = len(revs) - offset + 1
420
revision_id = branch.get_rev_id(revno, revs)
421
except errors.NoSuchRevision:
422
raise errors.InvalidRevisionSpec(self.user_spec, branch)
423
return RevisionInfo(branch, revno, revision_id)
425
SPEC_TYPES.append(RevisionSpec_last)
428
class RevisionSpec_before(RevisionSpec):
429
"""Selects the parent of the revision specified."""
431
help_txt = """Selects the parent of the revision specified.
433
Supply any revision spec to return the parent of that revision.
434
It is an error to request the parent of the null revision (before:0).
435
This is mostly useful when inspecting revisions that are not in the
436
revision history of a branch.
439
before:1913 -> Return the parent of revno 1913 (revno 1912)
440
before:revid:aaaa@bbbb-1234567890 -> return the parent of revision
442
bzr diff -r before:revid:aaaa..revid:aaaa
443
-> Find the changes between revision 'aaaa' and its parent.
444
(what changes did 'aaaa' introduce)
449
def _match_on(self, branch, revs):
450
r = RevisionSpec.from_string(self.spec)._match_on(branch, revs)
452
raise errors.InvalidRevisionSpec(self.user_spec, branch,
453
'cannot go before the null: revision')
455
# We need to use the repository history here
456
rev = branch.repository.get_revision(r.rev_id)
457
if not rev.parent_ids:
459
revision_id = revision.NULL_REVISION
461
revision_id = rev.parent_ids[0]
463
revno = revs.index(revision_id) + 1
469
revision_id = branch.get_rev_id(revno, revs)
470
except errors.NoSuchRevision:
471
raise errors.InvalidRevisionSpec(self.user_spec,
473
return RevisionInfo(branch, revno, revision_id)
475
SPEC_TYPES.append(RevisionSpec_before)
478
class RevisionSpec_tag(RevisionSpec):
479
"""Select a revision identified by tag name"""
481
help_txt = """Selects a revision identified by a tag name.
483
Tags are stored in the branch and created by the 'tag' command.
488
def _match_on(self, branch, revs):
489
# Can raise tags not supported, NoSuchTag, etc
490
return RevisionInfo.from_revision_id(branch,
491
branch.tags.lookup_tag(self.spec),
494
SPEC_TYPES.append(RevisionSpec_tag)
497
class _RevListToTimestamps(object):
498
"""This takes a list of revisions, and allows you to bisect by date"""
500
__slots__ = ['revs', 'branch']
502
def __init__(self, revs, branch):
506
def __getitem__(self, index):
507
"""Get the date of the index'd item"""
508
r = self.branch.repository.get_revision(self.revs[index])
509
# TODO: Handle timezone.
510
return datetime.datetime.fromtimestamp(r.timestamp)
513
return len(self.revs)
516
class RevisionSpec_date(RevisionSpec):
517
"""Selects a revision on the basis of a datestamp."""
519
help_txt = """Selects a revision on the basis of a datestamp.
521
Supply a datestamp to select the first revision that matches the date.
522
Date can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
523
Matches the first entry after a given date (either at midnight or
524
at a specified time).
526
One way to display all the changes since yesterday would be:
527
bzr log -r date:yesterday..-1
530
date:yesterday -> select the first revision since yesterday
531
date:2006-08-14,17:10:14 -> select the first revision after
532
August 14th, 2006 at 5:10pm.
535
_date_re = re.compile(
536
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
538
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
541
def _match_on(self, branch, revs):
542
"""Spec for date revisions:
544
value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
545
matches the first entry after a given date (either at midnight or
546
at a specified time).
548
# XXX: This doesn't actually work
549
# So the proper way of saying 'give me all entries for today' is:
550
# -r date:yesterday..date:today
551
today = datetime.datetime.fromordinal(datetime.date.today().toordinal())
552
if self.spec.lower() == 'yesterday':
553
dt = today - datetime.timedelta(days=1)
554
elif self.spec.lower() == 'today':
556
elif self.spec.lower() == 'tomorrow':
557
dt = today + datetime.timedelta(days=1)
559
m = self._date_re.match(self.spec)
560
if not m or (not m.group('date') and not m.group('time')):
561
raise errors.InvalidRevisionSpec(self.user_spec,
562
branch, 'invalid date')
566
year = int(m.group('year'))
567
month = int(m.group('month'))
568
day = int(m.group('day'))
575
hour = int(m.group('hour'))
576
minute = int(m.group('minute'))
577
if m.group('second'):
578
second = int(m.group('second'))
582
hour, minute, second = 0,0,0
584
raise errors.InvalidRevisionSpec(self.user_spec,
585
branch, 'invalid date')
587
dt = datetime.datetime(year=year, month=month, day=day,
588
hour=hour, minute=minute, second=second)
591
rev = bisect.bisect(_RevListToTimestamps(revs, branch), dt)
595
return RevisionInfo(branch, None)
597
return RevisionInfo(branch, rev + 1)
599
SPEC_TYPES.append(RevisionSpec_date)
602
class RevisionSpec_ancestor(RevisionSpec):
603
"""Selects a common ancestor with a second branch."""
605
help_txt = """Selects a common ancestor with a second branch.
607
Supply the path to a branch to select the common ancestor.
609
The common ancestor is the last revision that existed in both
610
branches. Usually this is the branch point, but it could also be
611
a revision that was merged.
613
This is frequently used with 'diff' to return all of the changes
614
that your branch introduces, while excluding the changes that you
615
have not merged from the remote branch.
618
ancestor:/path/to/branch
619
$ bzr diff -r ancestor:../../mainline/branch
623
def _match_on(self, branch, revs):
624
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
625
return self._find_revision_info(branch, self.spec)
628
def _find_revision_info(branch, other_location):
629
from bzrlib.branch import Branch
631
other_branch = Branch.open(other_location)
632
revision_a = branch.last_revision()
633
revision_b = other_branch.last_revision()
634
for r, b in ((revision_a, branch), (revision_b, other_branch)):
635
if r in (None, revision.NULL_REVISION):
636
raise errors.NoCommits(b)
637
revision_source = revision.MultipleRevisionSources(
638
branch.repository, other_branch.repository)
639
graph = branch.repository.get_graph(other_branch.repository)
640
revision_a = revision.ensure_null(revision_a)
641
revision_b = revision.ensure_null(revision_b)
642
if revision.NULL_REVISION in (revision_a, revision_b):
643
rev_id = revision.NULL_REVISION
645
rev_id = graph.find_unique_lca(revision_a, revision_b)
646
if rev_id == revision.NULL_REVISION:
647
raise errors.NoCommonAncestor(revision_a, revision_b)
649
revno = branch.revision_id_to_revno(rev_id)
650
except errors.NoSuchRevision:
652
return RevisionInfo(branch, revno, rev_id)
655
SPEC_TYPES.append(RevisionSpec_ancestor)
658
class RevisionSpec_branch(RevisionSpec):
659
"""Selects the last revision of a specified branch."""
661
help_txt = """Selects the last revision of a specified branch.
663
Supply the path to a branch to select its last revision.
666
branch:/path/to/branch
670
def _match_on(self, branch, revs):
671
from bzrlib.branch import Branch
672
other_branch = Branch.open(self.spec)
673
revision_b = other_branch.last_revision()
674
if revision_b in (None, revision.NULL_REVISION):
675
raise errors.NoCommits(other_branch)
676
# pull in the remote revisions so we can diff
677
branch.fetch(other_branch, revision_b)
679
revno = branch.revision_id_to_revno(revision_b)
680
except errors.NoSuchRevision:
682
return RevisionInfo(branch, revno, revision_b)
684
SPEC_TYPES.append(RevisionSpec_branch)
687
class RevisionSpec_submit(RevisionSpec_ancestor):
688
"""Selects a common ancestor with a submit branch."""
690
help_txt = """Selects a common ancestor with the submit branch.
692
Diffing against this shows all the changes that were made in this branch,
693
and is a good predictor of what merge will do. The submit branch is
694
used by the bundle and merge directive comands. If no submit branch
695
is specified, the parent branch is used instead.
697
The common ancestor is the last revision that existed in both
698
branches. Usually this is the branch point, but it could also be
699
a revision that was merged.
702
$ bzr diff -r submit:
707
def _match_on(self, branch, revs):
708
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
709
submit_location = branch.get_submit_branch()
710
location_type = 'submit branch'
711
if submit_location is None:
712
submit_location = branch.get_parent()
713
location_type = 'parent branch'
714
if submit_location is None:
715
raise errors.NoSubmitBranch(branch)
716
trace.note('Using %s %s', location_type, submit_location)
717
return self._find_revision_info(branch, submit_location)
720
SPEC_TYPES.append(RevisionSpec_submit)