1
# Copyright (C) 2005-2010 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
20
from .lazy_import import lazy_import
21
lazy_import(globals(), """
26
branch as _mod_branch,
32
from breezy.i18n import gettext
46
class RevisionInfo(object):
47
"""The results of applying a revision specification to a branch."""
49
help_txt = """The results of applying a revision specification to a branch.
51
An instance has two useful attributes: revno, and rev_id.
53
They can also be accessed as spec[0] and spec[1] respectively,
54
so that you can write code like:
55
revno, rev_id = RevisionSpec(branch, spec)
56
although this is probably going to be deprecated later.
58
This class exists mostly to be the return value of a RevisionSpec,
59
so that you can access the member you're interested in (number or id)
60
or treat the result as a tuple.
63
def __init__(self, branch, revno=None, rev_id=None):
65
self._has_revno = (revno is not None)
68
if self.rev_id is None and self._revno is not None:
69
# allow caller to be lazy
70
self.rev_id = branch.get_rev_id(self._revno)
74
if not self._has_revno and self.rev_id is not None:
76
self._revno = self.branch.revision_id_to_revno(self.rev_id)
77
except errors.NoSuchRevision:
79
self._has_revno = True
83
if self.rev_id is None:
85
# TODO: otherwise, it should depend on how I was built -
86
# if it's in_history(branch), then check revision_history(),
87
# if it's in_store(branch), do the check below
88
return self.branch.repository.has_revision(self.rev_id)
90
__nonzero__ = __bool__
95
def __getitem__(self, index):
96
if index == 0: return self.revno
97
if index == 1: return self.rev_id
98
raise IndexError(index)
101
return self.branch.repository.get_revision(self.rev_id)
103
def __eq__(self, other):
104
if type(other) not in (tuple, list, type(self)):
106
if isinstance(other, type(self)) and self.branch is not other.branch:
108
return tuple(self) == tuple(other)
111
return '<breezy.revisionspec.RevisionInfo object %s, %s for %r>' % (
112
self.revno, self.rev_id, self.branch)
115
def from_revision_id(branch, revision_id):
116
"""Construct a RevisionInfo given just the id.
118
Use this if you don't know or care what the revno is.
120
return RevisionInfo(branch, revno=None, rev_id=revision_id)
123
class RevisionSpec(object):
124
"""A parsed revision specification."""
126
help_txt = """A parsed revision specification.
128
A revision specification is a string, which may be unambiguous about
129
what it represents by giving a prefix like 'date:' or 'revid:' etc,
130
or it may have no prefix, in which case it's tried against several
131
specifier types in sequence to determine what the user meant.
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
dwim_catchable_exceptions = (errors.InvalidRevisionSpec,)
144
"""Exceptions that RevisionSpec_dwim._match_on will catch.
146
If the revspec is part of ``dwim_revspecs``, it may be tried with an
147
invalid revspec and raises some exception. The exceptions mentioned here
148
will not be reported to the user but simply ignored without stopping the
153
def from_string(spec):
154
"""Parse a revision spec string into a RevisionSpec object.
156
:param spec: A string specified by the user
157
:return: A RevisionSpec object that understands how to parse the
161
return RevisionSpec(None, _internal=True)
162
if not isinstance(spec, (str, text_type)):
163
raise TypeError("revision spec needs to be text")
164
match = revspec_registry.get_prefix(spec)
165
if match is not None:
166
spectype, specsuffix = match
167
trace.mutter('Returning RevisionSpec %s for %s',
168
spectype.__name__, spec)
169
return spectype(spec, _internal=True)
171
# Otherwise treat it as a DWIM, build the RevisionSpec object and
172
# wait for _match_on to be called.
173
return RevisionSpec_dwim(spec, _internal=True)
175
def __init__(self, spec, _internal=False):
176
"""Create a RevisionSpec referring to the Null revision.
178
:param spec: The original spec supplied by the user
179
:param _internal: Used to ensure that RevisionSpec is not being
180
called directly. Only from RevisionSpec.from_string()
183
raise AssertionError(
184
'Creating a RevisionSpec directly is not supported. '
185
'Use RevisionSpec.from_string() instead.')
186
self.user_spec = spec
187
if self.prefix and spec.startswith(self.prefix):
188
spec = spec[len(self.prefix):]
191
def _match_on(self, branch, revs):
192
trace.mutter('Returning RevisionSpec._match_on: None')
193
return RevisionInfo(branch, None, None)
195
def _match_on_and_check(self, branch, revs):
196
info = self._match_on(branch, revs)
199
elif info == (None, None):
200
# special case - nothing supplied
203
raise errors.InvalidRevisionSpec(self.user_spec, branch)
205
raise errors.InvalidRevisionSpec(self.spec, branch)
207
def in_history(self, branch):
208
return self._match_on_and_check(branch, revs=None)
210
# FIXME: in_history is somewhat broken,
211
# it will return non-history revisions in many
212
# circumstances. The expected facility is that
213
# in_history only returns revision-history revs,
214
# in_store returns any rev. RBC 20051010
215
# aliases for now, when we fix the core logic, then they
216
# will do what you expect.
217
in_store = in_history
220
def as_revision_id(self, context_branch):
221
"""Return just the revision_id for this revisions spec.
223
Some revision specs require a context_branch to be able to determine
224
their value. Not all specs will make use of it.
226
return self._as_revision_id(context_branch)
228
def _as_revision_id(self, context_branch):
229
"""Implementation of as_revision_id()
231
Classes should override this function to provide appropriate
232
functionality. The default is to just call '.in_history().rev_id'
234
return self.in_history(context_branch).rev_id
236
def as_tree(self, context_branch):
237
"""Return the tree object for this revisions spec.
239
Some revision specs require a context_branch to be able to determine
240
the revision id and access the repository. Not all specs will make
243
return self._as_tree(context_branch)
245
def _as_tree(self, context_branch):
246
"""Implementation of as_tree().
248
Classes should override this function to provide appropriate
249
functionality. The default is to just call '.as_revision_id()'
250
and get the revision tree from context_branch's repository.
252
revision_id = self.as_revision_id(context_branch)
253
return context_branch.repository.revision_tree(revision_id)
256
# this is mostly for helping with testing
257
return '<%s %s>' % (self.__class__.__name__,
260
def needs_branch(self):
261
"""Whether this revision spec needs a branch.
263
Set this to False the branch argument of _match_on is not used.
267
def get_branch(self):
268
"""When the revision specifier contains a branch location, return it.
270
Otherwise, return None.
277
class RevisionSpec_dwim(RevisionSpec):
278
"""Provides a DWIMish revision specifier lookup.
280
Note that this does not go in the revspec_registry because by definition
281
there is no prefix to identify it. It's solely called from
282
RevisionSpec.from_string() because the DWIMification happen when _match_on
283
is called so the string describing the revision is kept here until needed.
288
_revno_regex = lazy_regex.lazy_compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
290
# The revspecs to try
291
_possible_revspecs = []
293
def _try_spectype(self, rstype, branch):
294
rs = rstype(self.spec, _internal=True)
295
# Hit in_history to find out if it exists, or we need to try the
297
return rs.in_history(branch)
299
def _match_on(self, branch, revs):
300
"""Run the lookup and see what we can get."""
302
# First, see if it's a revno
303
if self._revno_regex.match(self.spec) is not None:
305
return self._try_spectype(RevisionSpec_revno, branch)
306
except RevisionSpec_revno.dwim_catchable_exceptions:
309
# Next see what has been registered
310
for objgetter in self._possible_revspecs:
311
rs_class = objgetter.get_obj()
313
return self._try_spectype(rs_class, branch)
314
except rs_class.dwim_catchable_exceptions:
317
# Well, I dunno what it is. Note that we don't try to keep track of the
318
# first of last exception raised during the DWIM tries as none seems
320
raise errors.InvalidRevisionSpec(self.spec, branch)
323
def append_possible_revspec(cls, revspec):
324
"""Append a possible DWIM revspec.
326
:param revspec: Revision spec to try.
328
cls._possible_revspecs.append(registry._ObjectGetter(revspec))
331
def append_possible_lazy_revspec(cls, module_name, member_name):
332
"""Append a possible lazily loaded DWIM revspec.
334
:param module_name: Name of the module with the revspec
335
:param member_name: Name of the revspec within the module
337
cls._possible_revspecs.append(
338
registry._LazyObjectGetter(module_name, member_name))
341
class RevisionSpec_revno(RevisionSpec):
342
"""Selects a revision using a number."""
344
help_txt = """Selects a revision using a number.
346
Use an integer to specify a revision in the history of the branch.
347
Optionally a branch can be specified. A negative number will count
348
from the end of the branch (-1 is the last revision, -2 the previous
349
one). If the negative number is larger than the branch's history, the
350
first revision is returned.
353
revno:1 -> return the first revision of this branch
354
revno:3:/path/to/branch -> return the 3rd revision of
355
the branch '/path/to/branch'
356
revno:-1 -> The last revision in a branch.
357
-2:http://other/branch -> The second to last revision in the
359
-1000000 -> Most likely the first revision, unless
360
your history is very long.
364
def _match_on(self, branch, revs):
365
"""Lookup a revision by revision number"""
366
branch, revno, revision_id = self._lookup(branch)
367
return RevisionInfo(branch, revno, revision_id)
369
def _lookup(self, branch):
370
loc = self.spec.find(':')
372
revno_spec = self.spec
375
revno_spec = self.spec[:loc]
376
branch_spec = self.spec[loc+1:]
380
raise errors.InvalidRevisionSpec(self.user_spec,
381
branch, 'cannot have an empty revno and no branch')
385
revno = int(revno_spec)
388
# dotted decimal. This arguably should not be here
389
# but the from_string method is a little primitive
390
# right now - RBC 20060928
392
match_revno = tuple((int(number) for number in revno_spec.split('.')))
393
except ValueError as e:
394
raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
399
# the user has overriden the branch to look in.
400
branch = _mod_branch.Branch.open(branch_spec)
404
revision_id = branch.dotted_revno_to_revision_id(match_revno,
406
except errors.NoSuchRevision:
407
raise errors.InvalidRevisionSpec(self.user_spec, branch)
409
# there is no traditional 'revno' for dotted-decimal revnos.
410
# so for API compatibility we return None.
411
return branch, None, revision_id
413
last_revno, last_revision_id = branch.last_revision_info()
415
# if get_rev_id supported negative revnos, there would not be a
416
# need for this special case.
417
if (-revno) >= last_revno:
420
revno = last_revno + revno + 1
422
revision_id = branch.get_rev_id(revno)
423
except errors.NoSuchRevision:
424
raise errors.InvalidRevisionSpec(self.user_spec, branch)
425
return branch, revno, revision_id
427
def _as_revision_id(self, context_branch):
428
# We would have the revno here, but we don't really care
429
branch, revno, revision_id = self._lookup(context_branch)
432
def needs_branch(self):
433
return self.spec.find(':') == -1
435
def get_branch(self):
436
if self.spec.find(':') == -1:
439
return self.spec[self.spec.find(':')+1:]
442
RevisionSpec_int = RevisionSpec_revno
445
class RevisionIDSpec(RevisionSpec):
447
def _match_on(self, branch, revs):
448
revision_id = self.as_revision_id(branch)
449
return RevisionInfo.from_revision_id(branch, revision_id)
452
class RevisionSpec_revid(RevisionIDSpec):
453
"""Selects a revision using the revision id."""
455
help_txt = """Selects a revision using the revision id.
457
Supply a specific revision id, that can be used to specify any
458
revision id in the ancestry of the branch.
459
Including merges, and pending merges.
462
revid:aaaa@bbbb-123456789 -> Select revision 'aaaa@bbbb-123456789'
467
def _as_revision_id(self, context_branch):
468
# self.spec comes straight from parsing the command line arguments,
469
# so we expect it to be a Unicode string. Switch it to the internal
471
if isinstance(self.spec, unicode):
472
return cache_utf8.encode(self.spec)
477
class RevisionSpec_last(RevisionSpec):
478
"""Selects the nth revision from the end."""
480
help_txt = """Selects the nth revision from the end.
482
Supply a positive number to get the nth revision from the end.
483
This is the same as supplying negative numbers to the 'revno:' spec.
486
last:1 -> return the last revision
487
last:3 -> return the revision 2 before the end.
492
def _match_on(self, branch, revs):
493
revno, revision_id = self._revno_and_revision_id(branch)
494
return RevisionInfo(branch, revno, revision_id)
496
def _revno_and_revision_id(self, context_branch):
497
last_revno, last_revision_id = context_branch.last_revision_info()
501
raise errors.NoCommits(context_branch)
502
return last_revno, last_revision_id
505
offset = int(self.spec)
506
except ValueError as e:
507
raise errors.InvalidRevisionSpec(self.user_spec, context_branch, e)
510
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
511
'you must supply a positive value')
513
revno = last_revno - offset + 1
515
revision_id = context_branch.get_rev_id(revno)
516
except errors.NoSuchRevision:
517
raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
518
return revno, revision_id
520
def _as_revision_id(self, context_branch):
521
# We compute the revno as part of the process, but we don't really care
523
revno, revision_id = self._revno_and_revision_id(context_branch)
528
class RevisionSpec_before(RevisionSpec):
529
"""Selects the parent of the revision specified."""
531
help_txt = """Selects the parent of the revision specified.
533
Supply any revision spec to return the parent of that revision. This is
534
mostly useful when inspecting revisions that are not in the revision history
537
It is an error to request the parent of the null revision (before:0).
541
before:1913 -> Return the parent of revno 1913 (revno 1912)
542
before:revid:aaaa@bbbb-1234567890 -> return the parent of revision
544
bzr diff -r before:1913..1913
545
-> Find the changes between revision 1913 and its parent (1912).
546
(What changes did revision 1913 introduce).
547
This is equivalent to: bzr diff -c 1913
552
def _match_on(self, branch, revs):
553
r = RevisionSpec.from_string(self.spec)._match_on(branch, revs)
555
raise errors.InvalidRevisionSpec(self.user_spec, branch,
556
'cannot go before the null: revision')
558
# We need to use the repository history here
559
rev = branch.repository.get_revision(r.rev_id)
560
if not rev.parent_ids:
561
revision_id = revision.NULL_REVISION
563
revision_id = rev.parent_ids[0]
568
revision_id = branch.get_rev_id(revno, revs)
569
except errors.NoSuchRevision:
570
raise errors.InvalidRevisionSpec(self.user_spec,
572
return RevisionInfo(branch, revno, revision_id)
574
def _as_revision_id(self, context_branch):
575
base_revision_id = RevisionSpec.from_string(self.spec)._as_revision_id(context_branch)
576
if base_revision_id == revision.NULL_REVISION:
577
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
578
'cannot go before the null: revision')
579
context_repo = context_branch.repository
580
context_repo.lock_read()
582
parent_map = context_repo.get_parent_map([base_revision_id])
584
context_repo.unlock()
585
if base_revision_id not in parent_map:
586
# Ghost, or unknown revision id
587
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
588
'cannot find the matching revision')
589
parents = parent_map[base_revision_id]
591
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
592
'No parents for revision.')
597
class RevisionSpec_tag(RevisionSpec):
598
"""Select a revision identified by tag name"""
600
help_txt = """Selects a revision identified by a tag name.
602
Tags are stored in the branch and created by the 'tag' command.
606
dwim_catchable_exceptions = (errors.NoSuchTag, errors.TagsNotSupported)
608
def _match_on(self, branch, revs):
609
# Can raise tags not supported, NoSuchTag, etc
610
return RevisionInfo.from_revision_id(branch,
611
branch.tags.lookup_tag(self.spec))
613
def _as_revision_id(self, context_branch):
614
return context_branch.tags.lookup_tag(self.spec)
618
class _RevListToTimestamps(object):
619
"""This takes a list of revisions, and allows you to bisect by date"""
621
__slots__ = ['branch']
623
def __init__(self, branch):
626
def __getitem__(self, index):
627
"""Get the date of the index'd item"""
628
r = self.branch.repository.get_revision(self.branch.get_rev_id(index))
629
# TODO: Handle timezone.
630
return datetime.datetime.fromtimestamp(r.timestamp)
633
return self.branch.revno()
636
class RevisionSpec_date(RevisionSpec):
637
"""Selects a revision on the basis of a datestamp."""
639
help_txt = """Selects a revision on the basis of a datestamp.
641
Supply a datestamp to select the first revision that matches the date.
642
Date can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
643
Matches the first entry after a given date (either at midnight or
644
at a specified time).
646
One way to display all the changes since yesterday would be::
648
brz log -r date:yesterday..
652
date:yesterday -> select the first revision since yesterday
653
date:2006-08-14,17:10:14 -> select the first revision after
654
August 14th, 2006 at 5:10pm.
657
_date_regex = lazy_regex.lazy_compile(
658
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
660
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
663
def _match_on(self, branch, revs):
664
"""Spec for date revisions:
666
value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
667
matches the first entry after a given date (either at midnight or
668
at a specified time).
670
# XXX: This doesn't actually work
671
# So the proper way of saying 'give me all entries for today' is:
672
# -r date:yesterday..date:today
673
today = datetime.datetime.fromordinal(datetime.date.today().toordinal())
674
if self.spec.lower() == 'yesterday':
675
dt = today - datetime.timedelta(days=1)
676
elif self.spec.lower() == 'today':
678
elif self.spec.lower() == 'tomorrow':
679
dt = today + datetime.timedelta(days=1)
681
m = self._date_regex.match(self.spec)
682
if not m or (not m.group('date') and not m.group('time')):
683
raise errors.InvalidRevisionSpec(self.user_spec,
684
branch, 'invalid date')
688
year = int(m.group('year'))
689
month = int(m.group('month'))
690
day = int(m.group('day'))
697
hour = int(m.group('hour'))
698
minute = int(m.group('minute'))
699
if m.group('second'):
700
second = int(m.group('second'))
704
hour, minute, second = 0, 0, 0
706
raise errors.InvalidRevisionSpec(self.user_spec,
707
branch, 'invalid date')
709
dt = datetime.datetime(year=year, month=month, day=day,
710
hour=hour, minute=minute, second=second)
711
with branch.lock_read():
712
rev = bisect.bisect(_RevListToTimestamps(branch), dt, 1)
713
if rev == branch.revno():
714
raise errors.InvalidRevisionSpec(self.user_spec, branch)
715
return RevisionInfo(branch, rev)
719
class RevisionSpec_ancestor(RevisionSpec):
720
"""Selects a common ancestor with a second branch."""
722
help_txt = """Selects a common ancestor with a second branch.
724
Supply the path to a branch to select the common ancestor.
726
The common ancestor is the last revision that existed in both
727
branches. Usually this is the branch point, but it could also be
728
a revision that was merged.
730
This is frequently used with 'diff' to return all of the changes
731
that your branch introduces, while excluding the changes that you
732
have not merged from the remote branch.
736
ancestor:/path/to/branch
737
$ bzr diff -r ancestor:../../mainline/branch
741
def _match_on(self, branch, revs):
742
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
743
return self._find_revision_info(branch, self.spec)
745
def _as_revision_id(self, context_branch):
746
return self._find_revision_id(context_branch, self.spec)
749
def _find_revision_info(branch, other_location):
750
revision_id = RevisionSpec_ancestor._find_revision_id(branch,
752
return RevisionInfo(branch, None, revision_id)
755
def _find_revision_id(branch, other_location):
756
from .branch import Branch
758
with branch.lock_read():
759
revision_a = revision.ensure_null(branch.last_revision())
760
if revision_a == revision.NULL_REVISION:
761
raise errors.NoCommits(branch)
762
if other_location == '':
763
other_location = branch.get_parent()
764
other_branch = Branch.open(other_location)
765
with other_branch.lock_read():
766
revision_b = revision.ensure_null(other_branch.last_revision())
767
if revision_b == revision.NULL_REVISION:
768
raise errors.NoCommits(other_branch)
769
graph = branch.repository.get_graph(other_branch.repository)
770
rev_id = graph.find_unique_lca(revision_a, revision_b)
771
if rev_id == revision.NULL_REVISION:
772
raise errors.NoCommonAncestor(revision_a, revision_b)
776
class RevisionSpec_branch(RevisionSpec):
777
"""Selects the last revision of a specified branch."""
779
help_txt = """Selects the last revision of a specified branch.
781
Supply the path to a branch to select its last revision.
785
branch:/path/to/branch
788
dwim_catchable_exceptions = (errors.NotBranchError,)
790
def _match_on(self, branch, revs):
791
from .branch import Branch
792
other_branch = Branch.open(self.spec)
793
revision_b = other_branch.last_revision()
794
if revision_b in (None, revision.NULL_REVISION):
795
raise errors.NoCommits(other_branch)
797
branch = other_branch
800
# pull in the remote revisions so we can diff
801
branch.fetch(other_branch, revision_b)
802
except errors.ReadOnlyError:
803
branch = other_branch
804
return RevisionInfo(branch, None, revision_b)
806
def _as_revision_id(self, context_branch):
807
from .branch import Branch
808
other_branch = Branch.open(self.spec)
809
last_revision = other_branch.last_revision()
810
last_revision = revision.ensure_null(last_revision)
811
context_branch.fetch(other_branch, last_revision)
812
if last_revision == revision.NULL_REVISION:
813
raise errors.NoCommits(other_branch)
816
def _as_tree(self, context_branch):
817
from .branch import Branch
818
other_branch = Branch.open(self.spec)
819
last_revision = other_branch.last_revision()
820
last_revision = revision.ensure_null(last_revision)
821
if last_revision == revision.NULL_REVISION:
822
raise errors.NoCommits(other_branch)
823
return other_branch.repository.revision_tree(last_revision)
825
def needs_branch(self):
828
def get_branch(self):
833
class RevisionSpec_submit(RevisionSpec_ancestor):
834
"""Selects a common ancestor with a submit branch."""
836
help_txt = """Selects a common ancestor with the submit branch.
838
Diffing against this shows all the changes that were made in this branch,
839
and is a good predictor of what merge will do. The submit branch is
840
used by the bundle and merge directive commands. If no submit branch
841
is specified, the parent branch is used instead.
843
The common ancestor is the last revision that existed in both
844
branches. Usually this is the branch point, but it could also be
845
a revision that was merged.
849
$ bzr diff -r submit:
854
def _get_submit_location(self, branch):
855
submit_location = branch.get_submit_branch()
856
location_type = 'submit branch'
857
if submit_location is None:
858
submit_location = branch.get_parent()
859
location_type = 'parent branch'
860
if submit_location is None:
861
raise errors.NoSubmitBranch(branch)
862
trace.note(gettext('Using {0} {1}').format(location_type,
864
return submit_location
866
def _match_on(self, branch, revs):
867
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
868
return self._find_revision_info(branch,
869
self._get_submit_location(branch))
871
def _as_revision_id(self, context_branch):
872
return self._find_revision_id(context_branch,
873
self._get_submit_location(context_branch))
876
class RevisionSpec_annotate(RevisionIDSpec):
880
help_txt = """Select the revision that last modified the specified line.
882
Select the revision that last modified the specified line. Line is
883
specified as path:number. Path is a relative path to the file. Numbers
884
start at 1, and are relative to the current version, not the last-
885
committed version of the file.
888
def _raise_invalid(self, numstring, context_branch):
889
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
890
'No such line: %s' % numstring)
892
def _as_revision_id(self, context_branch):
893
path, numstring = self.spec.rsplit(':', 1)
895
index = int(numstring) - 1
897
self._raise_invalid(numstring, context_branch)
898
tree, file_path = workingtree.WorkingTree.open_containing(path)
899
with tree.lock_read():
900
if not tree.has_filename(file_path):
901
raise errors.InvalidRevisionSpec(self.user_spec,
902
context_branch, "File '%s' is not versioned." %
904
revision_ids = [r for (r, l) in tree.annotate_iter(file_path)]
906
revision_id = revision_ids[index]
908
self._raise_invalid(numstring, context_branch)
909
if revision_id == revision.CURRENT_REVISION:
910
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
911
'Line %s has not been committed.' % numstring)
915
class RevisionSpec_mainline(RevisionIDSpec):
917
help_txt = """Select mainline revision that merged the specified revision.
919
Select the revision that merged the specified revision into mainline.
924
def _as_revision_id(self, context_branch):
925
revspec = RevisionSpec.from_string(self.spec)
926
if revspec.get_branch() is None:
927
spec_branch = context_branch
929
spec_branch = _mod_branch.Branch.open(revspec.get_branch())
930
revision_id = revspec.as_revision_id(spec_branch)
931
graph = context_branch.repository.get_graph()
932
result = graph.find_lefthand_merger(revision_id,
933
context_branch.last_revision())
935
raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
939
# The order in which we want to DWIM a revision spec without any prefix.
940
# revno is always tried first and isn't listed here, this is used by
941
# RevisionSpec_dwim._match_on
942
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_tag)
943
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_revid)
944
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_date)
945
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_branch)
947
revspec_registry = registry.Registry()
948
def _register_revspec(revspec):
949
revspec_registry.register(revspec.prefix, revspec)
951
_register_revspec(RevisionSpec_revno)
952
_register_revspec(RevisionSpec_revid)
953
_register_revspec(RevisionSpec_last)
954
_register_revspec(RevisionSpec_before)
955
_register_revspec(RevisionSpec_tag)
956
_register_revspec(RevisionSpec_date)
957
_register_revspec(RevisionSpec_ancestor)
958
_register_revspec(RevisionSpec_branch)
959
_register_revspec(RevisionSpec_submit)
960
_register_revspec(RevisionSpec_annotate)
961
_register_revspec(RevisionSpec_mainline)