summaryrefslogtreecommitdiff
path: root/bzrlib/revisionspec.py
diff options
context:
space:
mode:
authorLorry <lorry@roadtrain.codethink.co.uk>2012-08-22 15:47:16 +0100
committerLorry <lorry@roadtrain.codethink.co.uk>2012-08-22 15:47:16 +0100
commit25335618bf8755ce6b116ee14f47f5a1f2c821e9 (patch)
treed889d7ab3f9f985d0c54c534cb8052bd2e6d7163 /bzrlib/revisionspec.py
downloadbzr-tarball-25335618bf8755ce6b116ee14f47f5a1f2c821e9.tar.gz
Tarball conversion
Diffstat (limited to 'bzrlib/revisionspec.py')
-rw-r--r--bzrlib/revisionspec.py1009
1 files changed, 1009 insertions, 0 deletions
diff --git a/bzrlib/revisionspec.py b/bzrlib/revisionspec.py
new file mode 100644
index 0000000..b2c9ab5
--- /dev/null
+++ b/bzrlib/revisionspec.py
@@ -0,0 +1,1009 @@
+# Copyright (C) 2005-2010 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+from __future__ import absolute_import
+
+
+from bzrlib.lazy_import import lazy_import
+lazy_import(globals(), """
+import bisect
+import datetime
+
+from bzrlib import (
+ branch as _mod_branch,
+ osutils,
+ revision,
+ symbol_versioning,
+ workingtree,
+ )
+from bzrlib.i18n import gettext
+""")
+
+from bzrlib import (
+ errors,
+ lazy_regex,
+ registry,
+ trace,
+ )
+
+
+class RevisionInfo(object):
+ """The results of applying a revision specification to a branch."""
+
+ help_txt = """The results of applying a revision specification to a branch.
+
+ An instance has two useful attributes: revno, and rev_id.
+
+ They can also be accessed as spec[0] and spec[1] respectively,
+ so that you can write code like:
+ revno, rev_id = RevisionSpec(branch, spec)
+ although this is probably going to be deprecated later.
+
+ This class exists mostly to be the return value of a RevisionSpec,
+ so that you can access the member you're interested in (number or id)
+ or treat the result as a tuple.
+ """
+
+ def __init__(self, branch, revno=None, rev_id=None):
+ self.branch = branch
+ self._has_revno = (revno is not None)
+ self._revno = revno
+ self.rev_id = rev_id
+ if self.rev_id is None and self._revno is not None:
+ # allow caller to be lazy
+ self.rev_id = branch.get_rev_id(self._revno)
+
+ @property
+ def revno(self):
+ if not self._has_revno and self.rev_id is not None:
+ try:
+ self._revno = self.branch.revision_id_to_revno(self.rev_id)
+ except errors.NoSuchRevision:
+ self._revno = None
+ self._has_revno = True
+ return self._revno
+
+ def __nonzero__(self):
+ if self.rev_id is None:
+ return False
+ # TODO: otherwise, it should depend on how I was built -
+ # if it's in_history(branch), then check revision_history(),
+ # if it's in_store(branch), do the check below
+ return self.branch.repository.has_revision(self.rev_id)
+
+ def __len__(self):
+ return 2
+
+ def __getitem__(self, index):
+ if index == 0: return self.revno
+ if index == 1: return self.rev_id
+ raise IndexError(index)
+
+ def get(self):
+ return self.branch.repository.get_revision(self.rev_id)
+
+ def __eq__(self, other):
+ if type(other) not in (tuple, list, type(self)):
+ return False
+ if type(other) is type(self) and self.branch is not other.branch:
+ return False
+ return tuple(self) == tuple(other)
+
+ def __repr__(self):
+ return '<bzrlib.revisionspec.RevisionInfo object %s, %s for %r>' % (
+ self.revno, self.rev_id, self.branch)
+
+ @staticmethod
+ def from_revision_id(branch, revision_id, revs=symbol_versioning.DEPRECATED_PARAMETER):
+ """Construct a RevisionInfo given just the id.
+
+ Use this if you don't know or care what the revno is.
+ """
+ if symbol_versioning.deprecated_passed(revs):
+ symbol_versioning.warn(
+ 'RevisionInfo.from_revision_id(revs) was deprecated in 2.5.',
+ DeprecationWarning,
+ stacklevel=2)
+ return RevisionInfo(branch, revno=None, rev_id=revision_id)
+
+
+class RevisionSpec(object):
+ """A parsed revision specification."""
+
+ help_txt = """A parsed revision specification.
+
+ A revision specification is a string, which may be unambiguous about
+ what it represents by giving a prefix like 'date:' or 'revid:' etc,
+ or it may have no prefix, in which case it's tried against several
+ specifier types in sequence to determine what the user meant.
+
+ Revision specs are an UI element, and they have been moved out
+ of the branch class to leave "back-end" classes unaware of such
+ details. Code that gets a revno or rev_id from other code should
+ not be using revision specs - revnos and revision ids are the
+ accepted ways to refer to revisions internally.
+
+ (Equivalent to the old Branch method get_revision_info())
+ """
+
+ prefix = None
+ # wants_revision_history has been deprecated in 2.5.
+ wants_revision_history = False
+ dwim_catchable_exceptions = (errors.InvalidRevisionSpec,)
+ """Exceptions that RevisionSpec_dwim._match_on will catch.
+
+ If the revspec is part of ``dwim_revspecs``, it may be tried with an
+ invalid revspec and raises some exception. The exceptions mentioned here
+ will not be reported to the user but simply ignored without stopping the
+ dwim processing.
+ """
+
+ @staticmethod
+ def from_string(spec):
+ """Parse a revision spec string into a RevisionSpec object.
+
+ :param spec: A string specified by the user
+ :return: A RevisionSpec object that understands how to parse the
+ supplied notation.
+ """
+ if not isinstance(spec, (type(None), basestring)):
+ raise TypeError('error')
+
+ if spec is None:
+ return RevisionSpec(None, _internal=True)
+ match = revspec_registry.get_prefix(spec)
+ if match is not None:
+ spectype, specsuffix = match
+ trace.mutter('Returning RevisionSpec %s for %s',
+ spectype.__name__, spec)
+ return spectype(spec, _internal=True)
+ else:
+ # Otherwise treat it as a DWIM, build the RevisionSpec object and
+ # wait for _match_on to be called.
+ return RevisionSpec_dwim(spec, _internal=True)
+
+ def __init__(self, spec, _internal=False):
+ """Create a RevisionSpec referring to the Null revision.
+
+ :param spec: The original spec supplied by the user
+ :param _internal: Used to ensure that RevisionSpec is not being
+ called directly. Only from RevisionSpec.from_string()
+ """
+ if not _internal:
+ symbol_versioning.warn('Creating a RevisionSpec directly has'
+ ' been deprecated in version 0.11. Use'
+ ' RevisionSpec.from_string()'
+ ' instead.',
+ DeprecationWarning, stacklevel=2)
+ self.user_spec = spec
+ if self.prefix and spec.startswith(self.prefix):
+ spec = spec[len(self.prefix):]
+ self.spec = spec
+
+ def _match_on(self, branch, revs):
+ trace.mutter('Returning RevisionSpec._match_on: None')
+ return RevisionInfo(branch, None, None)
+
+ def _match_on_and_check(self, branch, revs):
+ info = self._match_on(branch, revs)
+ if info:
+ return info
+ elif info == (None, None):
+ # special case - nothing supplied
+ return info
+ elif self.prefix:
+ raise errors.InvalidRevisionSpec(self.user_spec, branch)
+ else:
+ raise errors.InvalidRevisionSpec(self.spec, branch)
+
+ def in_history(self, branch):
+ if branch:
+ if self.wants_revision_history:
+ symbol_versioning.warn(
+ "RevisionSpec.wants_revision_history was "
+ "deprecated in 2.5 (%s)." % self.__class__.__name__,
+ DeprecationWarning)
+ branch.lock_read()
+ try:
+ graph = branch.repository.get_graph()
+ revs = list(graph.iter_lefthand_ancestry(
+ branch.last_revision(), [revision.NULL_REVISION]))
+ finally:
+ branch.unlock()
+ revs.reverse()
+ else:
+ revs = None
+ else:
+ # this should never trigger.
+ # TODO: make it a deprecated code path. RBC 20060928
+ revs = None
+ return self._match_on_and_check(branch, revs)
+
+ # FIXME: in_history is somewhat broken,
+ # it will return non-history revisions in many
+ # circumstances. The expected facility is that
+ # in_history only returns revision-history revs,
+ # in_store returns any rev. RBC 20051010
+ # aliases for now, when we fix the core logic, then they
+ # will do what you expect.
+ in_store = in_history
+ in_branch = in_store
+
+ def as_revision_id(self, context_branch):
+ """Return just the revision_id for this revisions spec.
+
+ Some revision specs require a context_branch to be able to determine
+ their value. Not all specs will make use of it.
+ """
+ return self._as_revision_id(context_branch)
+
+ def _as_revision_id(self, context_branch):
+ """Implementation of as_revision_id()
+
+ Classes should override this function to provide appropriate
+ functionality. The default is to just call '.in_history().rev_id'
+ """
+ return self.in_history(context_branch).rev_id
+
+ def as_tree(self, context_branch):
+ """Return the tree object for this revisions spec.
+
+ Some revision specs require a context_branch to be able to determine
+ the revision id and access the repository. Not all specs will make
+ use of it.
+ """
+ return self._as_tree(context_branch)
+
+ def _as_tree(self, context_branch):
+ """Implementation of as_tree().
+
+ Classes should override this function to provide appropriate
+ functionality. The default is to just call '.as_revision_id()'
+ and get the revision tree from context_branch's repository.
+ """
+ revision_id = self.as_revision_id(context_branch)
+ return context_branch.repository.revision_tree(revision_id)
+
+ def __repr__(self):
+ # this is mostly for helping with testing
+ return '<%s %s>' % (self.__class__.__name__,
+ self.user_spec)
+
+ def needs_branch(self):
+ """Whether this revision spec needs a branch.
+
+ Set this to False the branch argument of _match_on is not used.
+ """
+ return True
+
+ def get_branch(self):
+ """When the revision specifier contains a branch location, return it.
+
+ Otherwise, return None.
+ """
+ return None
+
+
+# private API
+
+class RevisionSpec_dwim(RevisionSpec):
+ """Provides a DWIMish revision specifier lookup.
+
+ Note that this does not go in the revspec_registry because by definition
+ there is no prefix to identify it. It's solely called from
+ RevisionSpec.from_string() because the DWIMification happen when _match_on
+ is called so the string describing the revision is kept here until needed.
+ """
+
+ help_txt = None
+
+ _revno_regex = lazy_regex.lazy_compile(r'^(?:(\d+(\.\d+)*)|-\d+)(:.*)?$')
+
+ # The revspecs to try
+ _possible_revspecs = []
+
+ def _try_spectype(self, rstype, branch):
+ rs = rstype(self.spec, _internal=True)
+ # Hit in_history to find out if it exists, or we need to try the
+ # next type.
+ return rs.in_history(branch)
+
+ def _match_on(self, branch, revs):
+ """Run the lookup and see what we can get."""
+
+ # First, see if it's a revno
+ if self._revno_regex.match(self.spec) is not None:
+ try:
+ return self._try_spectype(RevisionSpec_revno, branch)
+ except RevisionSpec_revno.dwim_catchable_exceptions:
+ pass
+
+ # Next see what has been registered
+ for objgetter in self._possible_revspecs:
+ rs_class = objgetter.get_obj()
+ try:
+ return self._try_spectype(rs_class, branch)
+ except rs_class.dwim_catchable_exceptions:
+ pass
+
+ # Try the old (deprecated) dwim list:
+ for rs_class in dwim_revspecs:
+ try:
+ return self._try_spectype(rs_class, branch)
+ except rs_class.dwim_catchable_exceptions:
+ pass
+
+ # Well, I dunno what it is. Note that we don't try to keep track of the
+ # first of last exception raised during the DWIM tries as none seems
+ # really relevant.
+ raise errors.InvalidRevisionSpec(self.spec, branch)
+
+ @classmethod
+ def append_possible_revspec(cls, revspec):
+ """Append a possible DWIM revspec.
+
+ :param revspec: Revision spec to try.
+ """
+ cls._possible_revspecs.append(registry._ObjectGetter(revspec))
+
+ @classmethod
+ def append_possible_lazy_revspec(cls, module_name, member_name):
+ """Append a possible lazily loaded DWIM revspec.
+
+ :param module_name: Name of the module with the revspec
+ :param member_name: Name of the revspec within the module
+ """
+ cls._possible_revspecs.append(
+ registry._LazyObjectGetter(module_name, member_name))
+
+
+class RevisionSpec_revno(RevisionSpec):
+ """Selects a revision using a number."""
+
+ help_txt = """Selects a revision using a number.
+
+ Use an integer to specify a revision in the history of the branch.
+ Optionally a branch can be specified. A negative number will count
+ from the end of the branch (-1 is the last revision, -2 the previous
+ one). If the negative number is larger than the branch's history, the
+ first revision is returned.
+ Examples::
+
+ revno:1 -> return the first revision of this branch
+ revno:3:/path/to/branch -> return the 3rd revision of
+ the branch '/path/to/branch'
+ revno:-1 -> The last revision in a branch.
+ -2:http://other/branch -> The second to last revision in the
+ remote branch.
+ -1000000 -> Most likely the first revision, unless
+ your history is very long.
+ """
+ prefix = 'revno:'
+
+ def _match_on(self, branch, revs):
+ """Lookup a revision by revision number"""
+ branch, revno, revision_id = self._lookup(branch)
+ return RevisionInfo(branch, revno, revision_id)
+
+ def _lookup(self, branch):
+ loc = self.spec.find(':')
+ if loc == -1:
+ revno_spec = self.spec
+ branch_spec = None
+ else:
+ revno_spec = self.spec[:loc]
+ branch_spec = self.spec[loc+1:]
+
+ if revno_spec == '':
+ if not branch_spec:
+ raise errors.InvalidRevisionSpec(self.user_spec,
+ branch, 'cannot have an empty revno and no branch')
+ revno = None
+ else:
+ try:
+ revno = int(revno_spec)
+ dotted = False
+ except ValueError:
+ # dotted decimal. This arguably should not be here
+ # but the from_string method is a little primitive
+ # right now - RBC 20060928
+ try:
+ match_revno = tuple((int(number) for number in revno_spec.split('.')))
+ except ValueError, e:
+ raise errors.InvalidRevisionSpec(self.user_spec, branch, e)
+
+ dotted = True
+
+ if branch_spec:
+ # the user has overriden the branch to look in.
+ branch = _mod_branch.Branch.open(branch_spec)
+
+ if dotted:
+ try:
+ revision_id = branch.dotted_revno_to_revision_id(match_revno,
+ _cache_reverse=True)
+ except errors.NoSuchRevision:
+ raise errors.InvalidRevisionSpec(self.user_spec, branch)
+ else:
+ # there is no traditional 'revno' for dotted-decimal revnos.
+ # so for API compatibility we return None.
+ return branch, None, revision_id
+ else:
+ last_revno, last_revision_id = branch.last_revision_info()
+ if revno < 0:
+ # if get_rev_id supported negative revnos, there would not be a
+ # need for this special case.
+ if (-revno) >= last_revno:
+ revno = 1
+ else:
+ revno = last_revno + revno + 1
+ try:
+ revision_id = branch.get_rev_id(revno)
+ except errors.NoSuchRevision:
+ raise errors.InvalidRevisionSpec(self.user_spec, branch)
+ return branch, revno, revision_id
+
+ def _as_revision_id(self, context_branch):
+ # We would have the revno here, but we don't really care
+ branch, revno, revision_id = self._lookup(context_branch)
+ return revision_id
+
+ def needs_branch(self):
+ return self.spec.find(':') == -1
+
+ def get_branch(self):
+ if self.spec.find(':') == -1:
+ return None
+ else:
+ return self.spec[self.spec.find(':')+1:]
+
+# Old compatibility
+RevisionSpec_int = RevisionSpec_revno
+
+
+class RevisionIDSpec(RevisionSpec):
+
+ def _match_on(self, branch, revs):
+ revision_id = self.as_revision_id(branch)
+ return RevisionInfo.from_revision_id(branch, revision_id)
+
+
+class RevisionSpec_revid(RevisionIDSpec):
+ """Selects a revision using the revision id."""
+
+ help_txt = """Selects a revision using the revision id.
+
+ Supply a specific revision id, that can be used to specify any
+ revision id in the ancestry of the branch.
+ Including merges, and pending merges.
+ Examples::
+
+ revid:aaaa@bbbb-123456789 -> Select revision 'aaaa@bbbb-123456789'
+ """
+
+ prefix = 'revid:'
+
+ def _as_revision_id(self, context_branch):
+ # self.spec comes straight from parsing the command line arguments,
+ # so we expect it to be a Unicode string. Switch it to the internal
+ # representation.
+ return osutils.safe_revision_id(self.spec, warn=False)
+
+
+
+class RevisionSpec_last(RevisionSpec):
+ """Selects the nth revision from the end."""
+
+ help_txt = """Selects the nth revision from the end.
+
+ Supply a positive number to get the nth revision from the end.
+ This is the same as supplying negative numbers to the 'revno:' spec.
+ Examples::
+
+ last:1 -> return the last revision
+ last:3 -> return the revision 2 before the end.
+ """
+
+ prefix = 'last:'
+
+ def _match_on(self, branch, revs):
+ revno, revision_id = self._revno_and_revision_id(branch)
+ return RevisionInfo(branch, revno, revision_id)
+
+ def _revno_and_revision_id(self, context_branch):
+ last_revno, last_revision_id = context_branch.last_revision_info()
+
+ if self.spec == '':
+ if not last_revno:
+ raise errors.NoCommits(context_branch)
+ return last_revno, last_revision_id
+
+ try:
+ offset = int(self.spec)
+ except ValueError, e:
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch, e)
+
+ if offset <= 0:
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
+ 'you must supply a positive value')
+
+ revno = last_revno - offset + 1
+ try:
+ revision_id = context_branch.get_rev_id(revno)
+ except errors.NoSuchRevision:
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
+ return revno, revision_id
+
+ def _as_revision_id(self, context_branch):
+ # We compute the revno as part of the process, but we don't really care
+ # about it.
+ revno, revision_id = self._revno_and_revision_id(context_branch)
+ return revision_id
+
+
+
+class RevisionSpec_before(RevisionSpec):
+ """Selects the parent of the revision specified."""
+
+ help_txt = """Selects the parent of the revision specified.
+
+ Supply any revision spec to return the parent of that revision. This is
+ mostly useful when inspecting revisions that are not in the revision history
+ of a branch.
+
+ It is an error to request the parent of the null revision (before:0).
+
+ Examples::
+
+ before:1913 -> Return the parent of revno 1913 (revno 1912)
+ before:revid:aaaa@bbbb-1234567890 -> return the parent of revision
+ aaaa@bbbb-1234567890
+ bzr diff -r before:1913..1913
+ -> Find the changes between revision 1913 and its parent (1912).
+ (What changes did revision 1913 introduce).
+ This is equivalent to: bzr diff -c 1913
+ """
+
+ prefix = 'before:'
+
+ def _match_on(self, branch, revs):
+ r = RevisionSpec.from_string(self.spec)._match_on(branch, revs)
+ if r.revno == 0:
+ raise errors.InvalidRevisionSpec(self.user_spec, branch,
+ 'cannot go before the null: revision')
+ if r.revno is None:
+ # We need to use the repository history here
+ rev = branch.repository.get_revision(r.rev_id)
+ if not rev.parent_ids:
+ revision_id = revision.NULL_REVISION
+ else:
+ revision_id = rev.parent_ids[0]
+ revno = None
+ else:
+ revno = r.revno - 1
+ try:
+ revision_id = branch.get_rev_id(revno, revs)
+ except errors.NoSuchRevision:
+ raise errors.InvalidRevisionSpec(self.user_spec,
+ branch)
+ return RevisionInfo(branch, revno, revision_id)
+
+ def _as_revision_id(self, context_branch):
+ base_revision_id = RevisionSpec.from_string(self.spec)._as_revision_id(context_branch)
+ if base_revision_id == revision.NULL_REVISION:
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
+ 'cannot go before the null: revision')
+ context_repo = context_branch.repository
+ context_repo.lock_read()
+ try:
+ parent_map = context_repo.get_parent_map([base_revision_id])
+ finally:
+ context_repo.unlock()
+ if base_revision_id not in parent_map:
+ # Ghost, or unknown revision id
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
+ 'cannot find the matching revision')
+ parents = parent_map[base_revision_id]
+ if len(parents) < 1:
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
+ 'No parents for revision.')
+ return parents[0]
+
+
+
+class RevisionSpec_tag(RevisionSpec):
+ """Select a revision identified by tag name"""
+
+ help_txt = """Selects a revision identified by a tag name.
+
+ Tags are stored in the branch and created by the 'tag' command.
+ """
+
+ prefix = 'tag:'
+ dwim_catchable_exceptions = (errors.NoSuchTag, errors.TagsNotSupported)
+
+ def _match_on(self, branch, revs):
+ # Can raise tags not supported, NoSuchTag, etc
+ return RevisionInfo.from_revision_id(branch,
+ branch.tags.lookup_tag(self.spec))
+
+ def _as_revision_id(self, context_branch):
+ return context_branch.tags.lookup_tag(self.spec)
+
+
+
+class _RevListToTimestamps(object):
+ """This takes a list of revisions, and allows you to bisect by date"""
+
+ __slots__ = ['branch']
+
+ def __init__(self, branch):
+ self.branch = branch
+
+ def __getitem__(self, index):
+ """Get the date of the index'd item"""
+ r = self.branch.repository.get_revision(self.branch.get_rev_id(index))
+ # TODO: Handle timezone.
+ return datetime.datetime.fromtimestamp(r.timestamp)
+
+ def __len__(self):
+ return self.branch.revno()
+
+
+class RevisionSpec_date(RevisionSpec):
+ """Selects a revision on the basis of a datestamp."""
+
+ help_txt = """Selects a revision on the basis of a datestamp.
+
+ Supply a datestamp to select the first revision that matches the date.
+ Date can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
+ Matches the first entry after a given date (either at midnight or
+ at a specified time).
+
+ One way to display all the changes since yesterday would be::
+
+ bzr log -r date:yesterday..
+
+ Examples::
+
+ date:yesterday -> select the first revision since yesterday
+ date:2006-08-14,17:10:14 -> select the first revision after
+ August 14th, 2006 at 5:10pm.
+ """
+ prefix = 'date:'
+ _date_regex = lazy_regex.lazy_compile(
+ r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
+ r'(,|T)?\s*'
+ r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
+ )
+
+ def _match_on(self, branch, revs):
+ """Spec for date revisions:
+ date:value
+ value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
+ matches the first entry after a given date (either at midnight or
+ at a specified time).
+ """
+ # XXX: This doesn't actually work
+ # So the proper way of saying 'give me all entries for today' is:
+ # -r date:yesterday..date:today
+ today = datetime.datetime.fromordinal(datetime.date.today().toordinal())
+ if self.spec.lower() == 'yesterday':
+ dt = today - datetime.timedelta(days=1)
+ elif self.spec.lower() == 'today':
+ dt = today
+ elif self.spec.lower() == 'tomorrow':
+ dt = today + datetime.timedelta(days=1)
+ else:
+ m = self._date_regex.match(self.spec)
+ if not m or (not m.group('date') and not m.group('time')):
+ raise errors.InvalidRevisionSpec(self.user_spec,
+ branch, 'invalid date')
+
+ try:
+ if m.group('date'):
+ year = int(m.group('year'))
+ month = int(m.group('month'))
+ day = int(m.group('day'))
+ else:
+ year = today.year
+ month = today.month
+ day = today.day
+
+ if m.group('time'):
+ hour = int(m.group('hour'))
+ minute = int(m.group('minute'))
+ if m.group('second'):
+ second = int(m.group('second'))
+ else:
+ second = 0
+ else:
+ hour, minute, second = 0,0,0
+ except ValueError:
+ raise errors.InvalidRevisionSpec(self.user_spec,
+ branch, 'invalid date')
+
+ dt = datetime.datetime(year=year, month=month, day=day,
+ hour=hour, minute=minute, second=second)
+ branch.lock_read()
+ try:
+ rev = bisect.bisect(_RevListToTimestamps(branch), dt, 1)
+ finally:
+ branch.unlock()
+ if rev == branch.revno():
+ raise errors.InvalidRevisionSpec(self.user_spec, branch)
+ return RevisionInfo(branch, rev)
+
+
+
+class RevisionSpec_ancestor(RevisionSpec):
+ """Selects a common ancestor with a second branch."""
+
+ help_txt = """Selects a common ancestor with a second branch.
+
+ Supply the path to a branch to select the common ancestor.
+
+ The common ancestor is the last revision that existed in both
+ branches. Usually this is the branch point, but it could also be
+ a revision that was merged.
+
+ This is frequently used with 'diff' to return all of the changes
+ that your branch introduces, while excluding the changes that you
+ have not merged from the remote branch.
+
+ Examples::
+
+ ancestor:/path/to/branch
+ $ bzr diff -r ancestor:../../mainline/branch
+ """
+ prefix = 'ancestor:'
+
+ def _match_on(self, branch, revs):
+ trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
+ return self._find_revision_info(branch, self.spec)
+
+ def _as_revision_id(self, context_branch):
+ return self._find_revision_id(context_branch, self.spec)
+
+ @staticmethod
+ def _find_revision_info(branch, other_location):
+ revision_id = RevisionSpec_ancestor._find_revision_id(branch,
+ other_location)
+ return RevisionInfo(branch, None, revision_id)
+
+ @staticmethod
+ def _find_revision_id(branch, other_location):
+ from bzrlib.branch import Branch
+
+ branch.lock_read()
+ try:
+ revision_a = revision.ensure_null(branch.last_revision())
+ if revision_a == revision.NULL_REVISION:
+ raise errors.NoCommits(branch)
+ if other_location == '':
+ other_location = branch.get_parent()
+ other_branch = Branch.open(other_location)
+ other_branch.lock_read()
+ try:
+ revision_b = revision.ensure_null(other_branch.last_revision())
+ if revision_b == revision.NULL_REVISION:
+ raise errors.NoCommits(other_branch)
+ graph = branch.repository.get_graph(other_branch.repository)
+ rev_id = graph.find_unique_lca(revision_a, revision_b)
+ finally:
+ other_branch.unlock()
+ if rev_id == revision.NULL_REVISION:
+ raise errors.NoCommonAncestor(revision_a, revision_b)
+ return rev_id
+ finally:
+ branch.unlock()
+
+
+
+
+class RevisionSpec_branch(RevisionSpec):
+ """Selects the last revision of a specified branch."""
+
+ help_txt = """Selects the last revision of a specified branch.
+
+ Supply the path to a branch to select its last revision.
+
+ Examples::
+
+ branch:/path/to/branch
+ """
+ prefix = 'branch:'
+ dwim_catchable_exceptions = (errors.NotBranchError,)
+
+ def _match_on(self, branch, revs):
+ from bzrlib.branch import Branch
+ other_branch = Branch.open(self.spec)
+ revision_b = other_branch.last_revision()
+ if revision_b in (None, revision.NULL_REVISION):
+ raise errors.NoCommits(other_branch)
+ if branch is None:
+ branch = other_branch
+ else:
+ try:
+ # pull in the remote revisions so we can diff
+ branch.fetch(other_branch, revision_b)
+ except errors.ReadOnlyError:
+ branch = other_branch
+ return RevisionInfo(branch, None, revision_b)
+
+ def _as_revision_id(self, context_branch):
+ from bzrlib.branch import Branch
+ other_branch = Branch.open(self.spec)
+ last_revision = other_branch.last_revision()
+ last_revision = revision.ensure_null(last_revision)
+ context_branch.fetch(other_branch, last_revision)
+ if last_revision == revision.NULL_REVISION:
+ raise errors.NoCommits(other_branch)
+ return last_revision
+
+ def _as_tree(self, context_branch):
+ from bzrlib.branch import Branch
+ other_branch = Branch.open(self.spec)
+ last_revision = other_branch.last_revision()
+ last_revision = revision.ensure_null(last_revision)
+ if last_revision == revision.NULL_REVISION:
+ raise errors.NoCommits(other_branch)
+ return other_branch.repository.revision_tree(last_revision)
+
+ def needs_branch(self):
+ return False
+
+ def get_branch(self):
+ return self.spec
+
+
+
+class RevisionSpec_submit(RevisionSpec_ancestor):
+ """Selects a common ancestor with a submit branch."""
+
+ help_txt = """Selects a common ancestor with the submit branch.
+
+ Diffing against this shows all the changes that were made in this branch,
+ and is a good predictor of what merge will do. The submit branch is
+ used by the bundle and merge directive commands. If no submit branch
+ is specified, the parent branch is used instead.
+
+ The common ancestor is the last revision that existed in both
+ branches. Usually this is the branch point, but it could also be
+ a revision that was merged.
+
+ Examples::
+
+ $ bzr diff -r submit:
+ """
+
+ prefix = 'submit:'
+
+ def _get_submit_location(self, branch):
+ submit_location = branch.get_submit_branch()
+ location_type = 'submit branch'
+ if submit_location is None:
+ submit_location = branch.get_parent()
+ location_type = 'parent branch'
+ if submit_location is None:
+ raise errors.NoSubmitBranch(branch)
+ trace.note(gettext('Using {0} {1}').format(location_type,
+ submit_location))
+ return submit_location
+
+ def _match_on(self, branch, revs):
+ trace.mutter('matching ancestor: on: %s, %s', self.spec, branch)
+ return self._find_revision_info(branch,
+ self._get_submit_location(branch))
+
+ def _as_revision_id(self, context_branch):
+ return self._find_revision_id(context_branch,
+ self._get_submit_location(context_branch))
+
+
+class RevisionSpec_annotate(RevisionIDSpec):
+
+ prefix = 'annotate:'
+
+ help_txt = """Select the revision that last modified the specified line.
+
+ Select the revision that last modified the specified line. Line is
+ specified as path:number. Path is a relative path to the file. Numbers
+ start at 1, and are relative to the current version, not the last-
+ committed version of the file.
+ """
+
+ def _raise_invalid(self, numstring, context_branch):
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
+ 'No such line: %s' % numstring)
+
+ def _as_revision_id(self, context_branch):
+ path, numstring = self.spec.rsplit(':', 1)
+ try:
+ index = int(numstring) - 1
+ except ValueError:
+ self._raise_invalid(numstring, context_branch)
+ tree, file_path = workingtree.WorkingTree.open_containing(path)
+ tree.lock_read()
+ try:
+ file_id = tree.path2id(file_path)
+ if file_id is None:
+ raise errors.InvalidRevisionSpec(self.user_spec,
+ context_branch, "File '%s' is not versioned." %
+ file_path)
+ revision_ids = [r for (r, l) in tree.annotate_iter(file_id)]
+ finally:
+ tree.unlock()
+ try:
+ revision_id = revision_ids[index]
+ except IndexError:
+ self._raise_invalid(numstring, context_branch)
+ if revision_id == revision.CURRENT_REVISION:
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch,
+ 'Line %s has not been committed.' % numstring)
+ return revision_id
+
+
+class RevisionSpec_mainline(RevisionIDSpec):
+
+ help_txt = """Select mainline revision that merged the specified revision.
+
+ Select the revision that merged the specified revision into mainline.
+ """
+
+ prefix = 'mainline:'
+
+ def _as_revision_id(self, context_branch):
+ revspec = RevisionSpec.from_string(self.spec)
+ if revspec.get_branch() is None:
+ spec_branch = context_branch
+ else:
+ spec_branch = _mod_branch.Branch.open(revspec.get_branch())
+ revision_id = revspec.as_revision_id(spec_branch)
+ graph = context_branch.repository.get_graph()
+ result = graph.find_lefthand_merger(revision_id,
+ context_branch.last_revision())
+ if result is None:
+ raise errors.InvalidRevisionSpec(self.user_spec, context_branch)
+ return result
+
+
+# The order in which we want to DWIM a revision spec without any prefix.
+# revno is always tried first and isn't listed here, this is used by
+# RevisionSpec_dwim._match_on
+dwim_revspecs = symbol_versioning.deprecated_list(
+ symbol_versioning.deprecated_in((2, 4, 0)), "dwim_revspecs", [])
+
+RevisionSpec_dwim.append_possible_revspec(RevisionSpec_tag)
+RevisionSpec_dwim.append_possible_revspec(RevisionSpec_revid)
+RevisionSpec_dwim.append_possible_revspec(RevisionSpec_date)
+RevisionSpec_dwim.append_possible_revspec(RevisionSpec_branch)
+
+revspec_registry = registry.Registry()
+def _register_revspec(revspec):
+ revspec_registry.register(revspec.prefix, revspec)
+
+_register_revspec(RevisionSpec_revno)
+_register_revspec(RevisionSpec_revid)
+_register_revspec(RevisionSpec_last)
+_register_revspec(RevisionSpec_before)
+_register_revspec(RevisionSpec_tag)
+_register_revspec(RevisionSpec_date)
+_register_revspec(RevisionSpec_ancestor)
+_register_revspec(RevisionSpec_branch)
+_register_revspec(RevisionSpec_submit)
+_register_revspec(RevisionSpec_annotate)
+_register_revspec(RevisionSpec_mainline)