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)
713
rev = bisect.bisect(_RevListToTimestamps(branch), dt, 1)
716
if rev == branch.revno():
717
raise errors.InvalidRevisionSpec(self.user_spec, branch)
718
return RevisionInfo(branch, rev)
722
class RevisionSpec_ancestor(RevisionSpec):
723
"""Selects a common ancestor with a second branch."""
725
help_txt = """Selects a common ancestor with a second branch.
727
Supply the path to a branch to select the common ancestor.
729
The common ancestor is the last revision that existed in both
730
branches. Usually this is the branch point, but it could also be
731
a revision that was merged.
733
This is frequently used with 'diff' to return all of the changes
734
that your branch introduces, while excluding the changes that you
735
have not merged from the remote branch.
739
ancestor:/path/to/branch
740
$ bzr diff -r ancestor:../../mainline/branch
744
def _match_on(self, branch, revs):
745
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
746
return self._find_revision_info(branch, self.spec)
748
def _as_revision_id(self, context_branch):
749
return self._find_revision_id(context_branch, self.spec)
752
def _find_revision_info(branch, other_location):
753
revision_id = RevisionSpec_ancestor._find_revision_id(branch,
755
return RevisionInfo(branch, None, revision_id)
758
def _find_revision_id(branch, other_location):
759
from .branch import Branch
763
revision_a = revision.ensure_null(branch.last_revision())
764
if revision_a == revision.NULL_REVISION:
765
raise errors.NoCommits(branch)
766
if other_location == '':
767
other_location = branch.get_parent()
768
other_branch = Branch.open(other_location)
769
other_branch.lock_read()
771
revision_b = revision.ensure_null(other_branch.last_revision())
772
if revision_b == revision.NULL_REVISION:
773
raise errors.NoCommits(other_branch)
774
graph = branch.repository.get_graph(other_branch.repository)
775
rev_id = graph.find_unique_lca(revision_a, revision_b)
777
other_branch.unlock()
778
if rev_id == revision.NULL_REVISION:
779
raise errors.NoCommonAncestor(revision_a, revision_b)
787
class RevisionSpec_branch(RevisionSpec):
788
"""Selects the last revision of a specified branch."""
790
help_txt = """Selects the last revision of a specified branch.
792
Supply the path to a branch to select its last revision.
796
branch:/path/to/branch
799
dwim_catchable_exceptions = (errors.NotBranchError,)
801
def _match_on(self, branch, revs):
802
from .branch import Branch
803
other_branch = Branch.open(self.spec)
804
revision_b = other_branch.last_revision()
805
if revision_b in (None, revision.NULL_REVISION):
806
raise errors.NoCommits(other_branch)
808
branch = other_branch
811
# pull in the remote revisions so we can diff
812
branch.fetch(other_branch, revision_b)
813
except errors.ReadOnlyError:
814
branch = other_branch
815
return RevisionInfo(branch, None, revision_b)
817
def _as_revision_id(self, context_branch):
818
from .branch import Branch
819
other_branch = Branch.open(self.spec)
820
last_revision = other_branch.last_revision()
821
last_revision = revision.ensure_null(last_revision)
822
context_branch.fetch(other_branch, last_revision)
823
if last_revision == revision.NULL_REVISION:
824
raise errors.NoCommits(other_branch)
827
def _as_tree(self, context_branch):
828
from .branch import Branch
829
other_branch = Branch.open(self.spec)
830
last_revision = other_branch.last_revision()
831
last_revision = revision.ensure_null(last_revision)
832
if last_revision == revision.NULL_REVISION:
833
raise errors.NoCommits(other_branch)
834
return other_branch.repository.revision_tree(last_revision)
836
def needs_branch(self):
839
def get_branch(self):
844
class RevisionSpec_submit(RevisionSpec_ancestor):
845
"""Selects a common ancestor with a submit branch."""
847
help_txt = """Selects a common ancestor with the submit branch.
849
Diffing against this shows all the changes that were made in this branch,
850
and is a good predictor of what merge will do. The submit branch is
851
used by the bundle and merge directive commands. If no submit branch
852
is specified, the parent branch is used instead.
854
The common ancestor is the last revision that existed in both
855
branches. Usually this is the branch point, but it could also be
856
a revision that was merged.
860
$ bzr diff -r submit:
865
def _get_submit_location(self, branch):
866
submit_location = branch.get_submit_branch()
867
location_type = 'submit branch'
868
if submit_location is None:
869
submit_location = branch.get_parent()
870
location_type = 'parent branch'
871
if submit_location is None:
872
raise errors.NoSubmitBranch(branch)
873
trace.note(gettext('Using {0} {1}').format(location_type,
875
return submit_location
877
def _match_on(self, branch, revs):
878
trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
879
return self._find_revision_info(branch,
880
self._get_submit_location(branch))
882
def _as_revision_id(self, context_branch):
883
return self._find_revision_id(context_branch,
884
self._get_submit_location(context_branch))
887
class RevisionSpec_annotate(RevisionIDSpec):
891
help_txt = """Select the revision that last modified the specified line.
893
Select the revision that last modified the specified line. Line is
894
specified as path:number. Path is a relative path to the file. Numbers
895
start at 1, and are relative to the current version, not the last-
896
committed version of the file.
899
def _raise_invalid(self, numstring, context_branch):
900
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
901
'No such line: %s' % numstring)
903
def _as_revision_id(self, context_branch):
904
path, numstring = self.spec.rsplit(':', 1)
906
index = int(numstring) - 1
908
self._raise_invalid(numstring, context_branch)
909
tree, file_path = workingtree.WorkingTree.open_containing(path)
912
file_id = tree.path2id(file_path)
914
raise errors.InvalidRevisionSpec(self.user_spec,
915
context_branch, "File '%s' is not versioned." %
917
revision_ids = [r for (r, l) in tree.annotate_iter(file_id)]
921
revision_id = revision_ids[index]
923
self._raise_invalid(numstring, context_branch)
924
if revision_id == revision.CURRENT_REVISION:
925
raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
926
'Line %s has not been committed.' % numstring)
930
class RevisionSpec_mainline(RevisionIDSpec):
932
help_txt = """Select mainline revision that merged the specified revision.
934
Select the revision that merged the specified revision into mainline.
939
def _as_revision_id(self, context_branch):
940
revspec = RevisionSpec.from_string(self.spec)
941
if revspec.get_branch() is None:
942
spec_branch = context_branch
944
spec_branch = _mod_branch.Branch.open(revspec.get_branch())
945
revision_id = revspec.as_revision_id(spec_branch)
946
graph = context_branch.repository.get_graph()
947
result = graph.find_lefthand_merger(revision_id,
948
context_branch.last_revision())
950
raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
954
# The order in which we want to DWIM a revision spec without any prefix.
955
# revno is always tried first and isn't listed here, this is used by
956
# RevisionSpec_dwim._match_on
957
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_tag)
958
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_revid)
959
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_date)
960
RevisionSpec_dwim.append_possible_revspec(RevisionSpec_branch)
962
revspec_registry = registry.Registry()
963
def _register_revspec(revspec):
964
revspec_registry.register(revspec.prefix, revspec)
966
_register_revspec(RevisionSpec_revno)
967
_register_revspec(RevisionSpec_revid)
968
_register_revspec(RevisionSpec_last)
969
_register_revspec(RevisionSpec_before)
970
_register_revspec(RevisionSpec_tag)
971
_register_revspec(RevisionSpec_date)
972
_register_revspec(RevisionSpec_ancestor)
973
_register_revspec(RevisionSpec_branch)
974
_register_revspec(RevisionSpec_submit)
975
_register_revspec(RevisionSpec_annotate)
976
_register_revspec(RevisionSpec_mainline)