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(), """
34
from revision import NULL_REVISION
40
class RevisionInfo(object):
41
"""The results of applying a revision specification to a branch."""
43
help_txt = """The results of applying a revision specification to a branch.
45
An instance has two useful attributes: revno, and rev_id.
47
They can also be accessed as spec[0] and spec[1] respectively,
48
so that you can write code like:
49
revno, rev_id = RevisionSpec(branch, spec)
50
although this is probably going to be deprecated later.
52
This class exists mostly to be the return value of a RevisionSpec,
53
so that you can access the member you're interested in (number or id)
54
or treat the result as a tuple.
57
def __init__(self, branch, revno, rev_id=_marker):
61
# allow caller to be lazy
62
if self.revno is None:
63
self.rev_id = NULL_REVISION
65
self.rev_id = branch.get_rev_id(self.revno)
69
def __nonzero__(self):
70
# first the easy ones...
71
if self.rev_id is None:
73
if self.revno is not None:
75
# TODO: otherwise, it should depend on how I was built -
76
# if it's in_history(branch), then check revision_history(),
77
# if it's in_store(branch), do the check below
78
return self.branch.repository.has_revision(self.rev_id)
83
def __getitem__(self, index):
84
if index == 0: return self.revno
85
if index == 1: return self.rev_id
86
raise IndexError(index)
89
return self.branch.repository.get_revision(self.rev_id)
91
def __eq__(self, other):
92
if type(other) not in (tuple, list, type(self)):
94
if type(other) is type(self) and self.branch is not other.branch:
96
return tuple(self) == tuple(other)
99
return '<bzrlib.revisionspec.RevisionInfo object %s, %s for %r>' % (
100
self.revno, self.rev_id, self.branch)
103
def from_revision_id(branch, revision_id, revs):
104
"""Construct a RevisionInfo given just the id.
106
Use this if you don't know or care what the revno is.
108
if revision_id == revision.NULL_REVISION:
109
return RevisionInfo(branch, 0, revision_id)
111
revno = revs.index(revision_id) + 1
114
return RevisionInfo(branch, revno, revision_id)
117
# classes in this list should have a "prefix" attribute, against which
118
# string specs are matched
123
class RevisionSpec(object):
124
"""A parsed revision specification."""
126
help_txt = """A parsed revision specification.
128
A revision specification can be an integer, in which case it is
129
assumed to be a revno (though this will translate negative values
130
into positive ones); or it can be a string, in which case it is
131
parsed for something like 'date:' or 'revid:' etc.
133
Revision specs are an UI element, and they have been moved out
134
of the branch class to leave "back-end" classes unaware of such
135
details. Code that gets a revno or rev_id from other code should
136
not be using revision specs - revnos and revision ids are the
137
accepted ways to refer to revisions internally.
139
(Equivalent to the old Branch method get_revision_info())
143
wants_revision_history = True
146
def from_string(spec):
147
"""Parse a revision spec string into a RevisionSpec object.
149
:param spec: A string specified by the user
150
:return: A RevisionSpec object that understands how to parse the
153
if not isinstance(spec, (type(None), basestring)):
154
raise TypeError('error')
157
return RevisionSpec(None, _internal=True)
158
for spectype in SPEC_TYPES:
159
if spec.startswith(spectype.prefix):
160
trace.mutter('Returning RevisionSpec %s for %s',
161
spectype.__name__, spec)
162
return spectype(spec, _internal=True)
164
# RevisionSpec_revno is special cased, because it is the only
165
# one that directly handles plain integers
166
# TODO: This should not be special cased rather it should be
167
# a method invocation on spectype.canparse()
169
if _revno_regex is None:
170
_revno_regex = re.compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
171
if _revno_regex.match(spec) is not None:
172
return RevisionSpec_revno(spec, _internal=True)
174
raise errors.NoSuchRevisionSpec(spec)
176
def __init__(self, spec, _internal=False):
177
"""Create a RevisionSpec referring to the Null revision.
179
:param spec: The original spec supplied by the user
180
:param _internal: Used to ensure that RevisionSpec is not being
181
called directly. Only from RevisionSpec.from_string()
184
# XXX: Update this after 0.10 is released
185
symbol_versioning.warn('Creating a RevisionSpec directly has'
186
' been deprecated in version 0.11. Use'
187
' RevisionSpec.from_string()'
189
DeprecationWarning, stacklevel=2)
190
self.user_spec = spec
191
if self.prefix and spec.startswith(self.prefix):
192
spec = spec[len(self.prefix):]
195
def _match_on(self, branch, revs):
196
trace.mutter('Returning RevisionSpec._match_on: None')
197
return RevisionInfo(branch, None, None)
199
def _match_on_and_check(self, branch, revs):
200
info = self._match_on(branch, revs)
203
elif info == (None, None):
204
# special case - nothing supplied
207
raise errors.InvalidRevisionSpec(self.user_spec, branch)
209
raise errors.InvalidRevisionSpec(self.spec, branch)
211
def in_history(self, branch):
213
if self.wants_revision_history:
214
revs = branch.revision_history()
218
# this should never trigger.
219
# TODO: make it a deprecated code path. RBC 20060928
221
return self._match_on_and_check(branch, revs)
223
# FIXME: in_history is somewhat broken,
224
# it will return non-history revisions in many
225
# circumstances. The expected facility is that
226
# in_history only returns revision-history revs,
227
# in_store returns any rev. RBC 20051010
228
# aliases for now, when we fix the core logic, then they
229
# will do what you expect.
230
in_store = in_history
233
def as_revision_id(self, context_branch):
234
"""Return just the revision_id for this revisions spec.
236
Some revision specs require a context_branch to be able to determine
237
their value. Not all specs will make use of it.
239
return self._as_revision_id(context_branch)
241
def _as_revision_id(self, context_branch):
242
"""Implementation of as_revision_id()
244
Classes should override this function to provide appropriate
245
functionality. The default is to just call '.in_history().rev_id'
247
return self.in_history(context_branch).rev_id
249
def as_tree(self, context_branch):
250
"""Return the tree object for this revisions spec.
252
Some revision specs require a context_branch to be able to determine
253
the revision id and access the repository. Not all specs will make
256
return self._as_tree(context_branch)
258
def _as_tree(self, context_branch):
259
"""Implementation of as_tree().
261
Classes should override this function to provide appropriate
262
functionality. The default is to just call '.as_revision_id()'
263
and get the revision tree from context_branch's repository.
265
revision_id = self.as_revision_id(context_branch)
266
return context_branch.repository.revision_tree(revision_id)
269
# this is mostly for helping with testing
270
return '<%s %s>' % (self.__class__.__name__,
273
def needs_branch(self):
274
"""Whether this revision spec needs a branch.
276
Set this to False the branch argument of _match_on is not used.
280
def get_branch(self):
281
"""When the revision specifier contains a branch location, return it.
283
Otherwise, return None.
290
class RevisionSpec_revno(RevisionSpec):
291
"""Selects a revision using a number."""
293
help_txt = """Selects a revision using a number.
295
Use an integer to specify a revision in the history of the branch.
296
Optionally a branch can be specified. The 'revno:' prefix is optional.
297
A negative number will count from the end of the branch (-1 is the
298
last revision, -2 the previous one). If the negative number is larger
299
than the branch's history, the first revision is returned.
302
revno:1 -> return the first revision of this branch
303
revno:3:/path/to/branch -> return the 3rd revision of
304
the branch '/path/to/branch'
305
revno:-1 -> The last revision in a branch.
306
-2:http://other/branch -> The second to last revision in the
308
-1000000 -> Most likely the first revision, unless
309
your history is very long.
312
wants_revision_history = False
314
def _match_on(self, branch, revs):
315
"""Lookup a revision by revision number"""
316
branch, revno, revision_id = self._lookup(branch, revs)
317
return RevisionInfo(branch, revno, revision_id)
319
def _lookup(self, branch, revs_or_none):
320
loc = self.spec.find(':')
322
revno_spec = self.spec
325
revno_spec = self.spec[:loc]
326
branch_spec = self.spec[loc+1:]
330
raise errors.InvalidRevisionSpec(self.user_spec,
331
branch, 'cannot have an empty revno and no branch')
335
revno = int(revno_spec)
338
# dotted decimal. This arguably should not be here
339
# but the from_string method is a little primitive
340
# right now - RBC 20060928
342
match_revno = tuple((int(number) for number in revno_spec.split('.')))
343
except ValueError, e:
344
raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
349
# the user has override the branch to look in.
350
# we need to refresh the revision_history map and
352
from bzrlib.branch import Branch
353
branch = Branch.open(branch_spec)
359
revision_id_to_revno = branch.get_revision_id_to_revno_map()
360
revisions = [revision_id for revision_id, revno
361
in revision_id_to_revno.iteritems()
362
if revno == match_revno]
365
if len(revisions) != 1:
366
raise errors.InvalidRevisionSpec(self.user_spec, branch)
368
# there is no traditional 'revno' for dotted-decimal revnos.
369
# so for API compatability we return None.
370
return branch, None, revisions[0]
372
last_revno, last_revision_id = branch.last_revision_info()
374
# if get_rev_id supported negative revnos, there would not be a
375
# need for this special case.
376
if (-revno) >= last_revno:
379
revno = last_revno + revno + 1
381
revision_id = branch.get_rev_id(revno, revs_or_none)
382
except errors.NoSuchRevision:
383
raise errors.InvalidRevisionSpec(self.user_spec, branch)
384
return branch, revno, revision_id
386
def _as_revision_id(self, context_branch):
387
# We would have the revno here, but we don't really care
388
branch, revno, revision_id = self._lookup(context_branch, None)
391
def needs_branch(self):
392
return self.spec.find(':') == -1
394
def get_branch(self):
395
if self.spec.find(':') == -1:
398
return self.spec[self.spec.find(':')+1:]
401
RevisionSpec_int = RevisionSpec_revno
403
SPEC_TYPES.append(RevisionSpec_revno)
406
class RevisionSpec_revid(RevisionSpec):
407
"""Selects a revision using the revision id."""
409
help_txt = """Selects a revision using the revision id.
411
Supply a specific revision id, that can be used to specify any
412
revision id in the ancestry of the branch.
413
Including merges, and pending merges.
416
revid:aaaa@bbbb-123456789 -> Select revision 'aaaa@bbbb-123456789'
421
def _match_on(self, branch, revs):
422
# self.spec comes straight from parsing the command line arguments,
423
# so we expect it to be a Unicode string. Switch it to the internal
425
revision_id = osutils.safe_revision_id(self.spec, warn=False)
426
return RevisionInfo.from_revision_id(branch, revision_id, revs)
428
def _as_revision_id(self, context_branch):
429
return osutils.safe_revision_id(self.spec, warn=False)
431
SPEC_TYPES.append(RevisionSpec_revid)
434
class RevisionSpec_last(RevisionSpec):
435
"""Selects the nth revision from the end."""
437
help_txt = """Selects the nth revision from the end.
439
Supply a positive number to get the nth revision from the end.
440
This is the same as supplying negative numbers to the 'revno:' spec.
443
last:1 -> return the last revision
444
last:3 -> return the revision 2 before the end.
449
def _match_on(self, branch, revs):
450
revno, revision_id = self._revno_and_revision_id(branch, revs)
451
return RevisionInfo(branch, revno, revision_id)
453
def _revno_and_revision_id(self, context_branch, revs_or_none):
454
last_revno, last_revision_id = context_branch.last_revision_info()
458
raise errors.NoCommits(context_branch)
459
return last_revno, last_revision_id
462
offset = int(self.spec)
463
except ValueError, e:
464
raise errors.InvalidRevisionSpec(self.user_spec, context_branch, e)
467
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
468
'you must supply a positive value')
470
revno = last_revno - offset + 1
472
revision_id = context_branch.get_rev_id(revno, revs_or_none)
473
except errors.NoSuchRevision:
474
raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
475
return revno, revision_id
477
def _as_revision_id(self, context_branch):
478
# We compute the revno as part of the process, but we don't really care
480
revno, revision_id = self._revno_and_revision_id(context_branch, None)
483
SPEC_TYPES.append(RevisionSpec_last)
486
class RevisionSpec_before(RevisionSpec):
487
"""Selects the parent of the revision specified."""
489
help_txt = """Selects the parent of the revision specified.
491
Supply any revision spec to return the parent of that revision. This is
492
mostly useful when inspecting revisions that are not in the revision history
495
It is an error to request the parent of the null revision (before:0).
499
before:1913 -> Return the parent of revno 1913 (revno 1912)
500
before:revid:aaaa@bbbb-1234567890 -> return the parent of revision
502
bzr diff -r before:1913..1913
503
-> Find the changes between revision 1913 and its parent (1912).
504
(What changes did revision 1913 introduce).
505
This is equivalent to: bzr diff -c 1913
510
def _match_on(self, branch, revs):
511
r = RevisionSpec.from_string(self.spec)._match_on(branch, revs)
513
raise errors.InvalidRevisionSpec(self.user_spec, branch,
514
'cannot go before the null: revision')
516
# We need to use the repository history here
517
rev = branch.repository.get_revision(r.rev_id)
518
if not rev.parent_ids:
520
revision_id = revision.NULL_REVISION
522
revision_id = rev.parent_ids[0]
524
revno = revs.index(revision_id) + 1
530
revision_id = branch.get_rev_id(revno, revs)
531
except errors.NoSuchRevision:
532
raise errors.InvalidRevisionSpec(self.user_spec,
534
return RevisionInfo(branch, revno, revision_id)
536
def _as_revision_id(self, context_branch):
537
base_revspec = RevisionSpec.from_string(self.spec)
538
base_revision_id = base_revspec.as_revision_id(context_branch)
539
if base_revision_id == revision.NULL_REVISION:
540
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
541
'cannot go before the null: revision')
542
context_repo = context_branch.repository
543
context_repo.lock_read()
545
parent_map = context_repo.get_parent_map([base_revision_id])
547
context_repo.unlock()
548
if base_revision_id not in parent_map:
549
# Ghost, or unknown revision id
550
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
551
'cannot find the matching revision')
552
parents = parent_map[base_revision_id]
554
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
555
'No parents for revision.')
558
SPEC_TYPES.append(RevisionSpec_before)
561
class RevisionSpec_tag(RevisionSpec):
562
"""Select a revision identified by tag name"""
564
help_txt = """Selects a revision identified by a tag name.
566
Tags are stored in the branch and created by the 'tag' command.
571
def _match_on(self, branch, revs):
572
# Can raise tags not supported, NoSuchTag, etc
573
return RevisionInfo.from_revision_id(branch,
574
branch.tags.lookup_tag(self.spec),
577
def _as_revision_id(self, context_branch):
578
return context_branch.tags.lookup_tag(self.spec)
580
SPEC_TYPES.append(RevisionSpec_tag)
583
class _RevListToTimestamps(object):
584
"""This takes a list of revisions, and allows you to bisect by date"""
586
__slots__ = ['revs', 'branch']
588
def __init__(self, revs, branch):
592
def __getitem__(self, index):
593
"""Get the date of the index'd item"""
594
r = self.branch.repository.get_revision(self.revs[index])
595
# TODO: Handle timezone.
596
return datetime.datetime.fromtimestamp(r.timestamp)
599
return len(self.revs)
602
class RevisionSpec_date(RevisionSpec):
603
"""Selects a revision on the basis of a datestamp."""
605
help_txt = """Selects a revision on the basis of a datestamp.
607
Supply a datestamp to select the first revision that matches the date.
608
Date can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
609
Matches the first entry after a given date (either at midnight or
610
at a specified time).
612
One way to display all the changes since yesterday would be::
614
bzr log -r date:yesterday..
618
date:yesterday -> select the first revision since yesterday
619
date:2006-08-14,17:10:14 -> select the first revision after
620
August 14th, 2006 at 5:10pm.
623
_date_re = re.compile(
624
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
626
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
629
def _match_on(self, branch, revs):
630
"""Spec for date revisions:
632
value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
633
matches the first entry after a given date (either at midnight or
634
at a specified time).
636
# XXX: This doesn't actually work
637
# So the proper way of saying 'give me all entries for today' is:
638
# -r date:yesterday..date:today
639
today = datetime.datetime.fromordinal(datetime.date.today().toordinal())
640
if self.spec.lower() == 'yesterday':
641
dt = today - datetime.timedelta(days=1)
642
elif self.spec.lower() == 'today':
644
elif self.spec.lower() == 'tomorrow':
645
dt = today + datetime.timedelta(days=1)
647
m = self._date_re.match(self.spec)
648
if not m or (not m.group('date') and not m.group('time')):
649
raise errors.InvalidRevisionSpec(self.user_spec,
650
branch, 'invalid date')
654
year = int(m.group('year'))
655
month = int(m.group('month'))
656
day = int(m.group('day'))
663
hour = int(m.group('hour'))
664
minute = int(m.group('minute'))
665
if m.group('second'):
666
second = int(m.group('second'))
670
hour, minute, second = 0,0,0
672
raise errors.InvalidRevisionSpec(self.user_spec,
673
branch, 'invalid date')
675
dt = datetime.datetime(year=year, month=month, day=day,
676
hour=hour, minute=minute, second=second)
679
rev = bisect.bisect(_RevListToTimestamps(revs, branch), dt)
683
raise errors.InvalidRevisionSpec(self.user_spec, branch)
685
return RevisionInfo(branch, rev + 1)
687
SPEC_TYPES.append(RevisionSpec_date)
690
class RevisionSpec_ancestor(RevisionSpec):
691
"""Selects a common ancestor with a second branch."""
693
help_txt = """Selects a common ancestor with a second branch.
695
Supply the path to a branch to select the common ancestor.
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.
701
This is frequently used with 'diff' to return all of the changes
702
that your branch introduces, while excluding the changes that you
703
have not merged from the remote branch.
707
ancestor:/path/to/branch
708
$ bzr diff -r ancestor:../../mainline/branch
712
def _match_on(self, branch, revs):
713
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
714
return self._find_revision_info(branch, self.spec)
716
def _as_revision_id(self, context_branch):
717
return self._find_revision_id(context_branch, self.spec)
720
def _find_revision_info(branch, other_location):
721
revision_id = RevisionSpec_ancestor._find_revision_id(branch,
724
revno = branch.revision_id_to_revno(revision_id)
725
except errors.NoSuchRevision:
727
return RevisionInfo(branch, revno, revision_id)
730
def _find_revision_id(branch, other_location):
731
from bzrlib.branch import Branch
735
revision_a = revision.ensure_null(branch.last_revision())
736
if revision_a == revision.NULL_REVISION:
737
raise errors.NoCommits(branch)
738
other_branch = Branch.open(other_location)
739
other_branch.lock_read()
741
revision_b = revision.ensure_null(other_branch.last_revision())
742
if revision_b == revision.NULL_REVISION:
743
raise errors.NoCommits(other_branch)
744
graph = branch.repository.get_graph(other_branch.repository)
745
rev_id = graph.find_unique_lca(revision_a, revision_b)
747
other_branch.unlock()
748
if rev_id == revision.NULL_REVISION:
749
raise errors.NoCommonAncestor(revision_a, revision_b)
755
SPEC_TYPES.append(RevisionSpec_ancestor)
758
class RevisionSpec_branch(RevisionSpec):
759
"""Selects the last revision of a specified branch."""
761
help_txt = """Selects the last revision of a specified branch.
763
Supply the path to a branch to select its last revision.
767
branch:/path/to/branch
771
def _match_on(self, branch, revs):
772
from bzrlib.branch import Branch
773
other_branch = Branch.open(self.spec)
774
revision_b = other_branch.last_revision()
775
if revision_b in (None, revision.NULL_REVISION):
776
raise errors.NoCommits(other_branch)
777
# pull in the remote revisions so we can diff
778
branch.fetch(other_branch, revision_b)
780
revno = branch.revision_id_to_revno(revision_b)
781
except errors.NoSuchRevision:
783
return RevisionInfo(branch, revno, revision_b)
785
def _as_revision_id(self, context_branch):
786
from bzrlib.branch import Branch
787
other_branch = Branch.open(self.spec)
788
last_revision = other_branch.last_revision()
789
last_revision = revision.ensure_null(last_revision)
790
context_branch.fetch(other_branch, last_revision)
791
if last_revision == revision.NULL_REVISION:
792
raise errors.NoCommits(other_branch)
795
def _as_tree(self, context_branch):
796
from bzrlib.branch import Branch
797
other_branch = Branch.open(self.spec)
798
last_revision = other_branch.last_revision()
799
last_revision = revision.ensure_null(last_revision)
800
if last_revision == revision.NULL_REVISION:
801
raise errors.NoCommits(other_branch)
802
return other_branch.repository.revision_tree(last_revision)
804
SPEC_TYPES.append(RevisionSpec_branch)
807
class RevisionSpec_submit(RevisionSpec_ancestor):
808
"""Selects a common ancestor with a submit branch."""
810
help_txt = """Selects a common ancestor with the submit branch.
812
Diffing against this shows all the changes that were made in this branch,
813
and is a good predictor of what merge will do. The submit branch is
814
used by the bundle and merge directive commands. If no submit branch
815
is specified, the parent branch is used instead.
817
The common ancestor is the last revision that existed in both
818
branches. Usually this is the branch point, but it could also be
819
a revision that was merged.
823
$ bzr diff -r submit:
828
def _get_submit_location(self, branch):
829
submit_location = branch.get_submit_branch()
830
location_type = 'submit branch'
831
if submit_location is None:
832
submit_location = branch.get_parent()
833
location_type = 'parent branch'
834
if submit_location is None:
835
raise errors.NoSubmitBranch(branch)
836
trace.note('Using %s %s', location_type, submit_location)
837
return submit_location
839
def _match_on(self, branch, revs):
840
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
841
return self._find_revision_info(branch,
842
self._get_submit_location(branch))
844
def _as_revision_id(self, context_branch):
845
return self._find_revision_id(context_branch,
846
self._get_submit_location(context_branch))
849
SPEC_TYPES.append(RevisionSpec_submit)