# 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 '' % ( 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(?P\d\d\d\d)-(?P\d\d)-(?P\d\d))?' r'(,|T)?\s*' r'(?P