--- a/hgext/evolve.py Thu Aug 23 18:21:18 2012 +0200
+++ b/hgext/evolve.py Fri Aug 24 10:44:23 2012 +0200
@@ -7,25 +7,1096 @@
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
-'''a set of commands to handle changeset mutation'''
+'''Extends Mercurial feature related to Changeset Evolution
+
+This extension Provide several command tommutate history and deal with issue it may raise.
+
+It also:
+
+ - enable the "Changeset Obsolescence" feature of mercurial,
+ - alter core command and extension that rewrite history to use this feature,
+ - improve some aspect of the early implementation in 2.3
+'''
import random
+from mercurial import util
+
+try:
+ from mercurial import obsolete
+ if not obsolete._enabled:
+ obsolete._enabled = True
+except ImportError:
+ raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)')
+
from mercurial import bookmarks
from mercurial import cmdutil
from mercurial import commands
from mercurial import context
from mercurial import copies
+from mercurial import discovery
from mercurial import error
from mercurial import extensions
+from mercurial import hg
+from mercurial import localrepo
from mercurial import merge
from mercurial import node
from mercurial import phases
+from mercurial import revset
from mercurial import scmutil
-from mercurial import util
+from mercurial import templatekw
from mercurial.i18n import _
from mercurial.commands import walkopts, commitopts, commitopts2
-from mercurial import hg
+from mercurial.node import nullid
+
+
+
+# This extension contains the following code
+#
+# - Extension Helper code
+# - Obsolescence cache
+# - ...
+# - Older format compat
+
+
+
+#####################################################################
+### Extension helper ###
+#####################################################################
+
+class exthelper(object):
+ """Helper for modular extension setup
+
+ A single helper should be instanciated for each extension. Helper
+ methods are then used as decorator for various purpose.
+
+ All decorators return the original function and may be chained.
+ """
+
+ def __init__(self):
+ self._uicallables = []
+ self._extcallables = []
+ self._repocallables = []
+ self._revsetsymbols = []
+ self._templatekws = []
+ self._commandwrappers = []
+ self._extcommandwrappers = []
+ self._functionwrappers = []
+ self._duckpunchers = []
+
+ def final_uisetup(self, ui):
+ """Method to be used as the extension uisetup
+
+ The following operations belong here:
+
+ - Changes to ui.__class__ . The ui object that will be used to run the
+ command has not yet been created. Changes made here will affect ui
+ objects created after this, and in particular the ui that will be
+ passed to runcommand
+ - Command wraps (extensions.wrapcommand)
+ - Changes that need to be visible to other extensions: because
+ initialization occurs in phases (all extensions run uisetup, then all
+ run extsetup), a change made here will be visible to other extensions
+ during extsetup
+ - Monkeypatch or wrap function (extensions.wrapfunction) of dispatch
+ module members
+ - Setup of pre-* and post-* hooks
+ - pushkey setup
+ """
+ for cont, funcname, func in self._duckpunchers:
+ setattr(cont, funcname, func)
+ for command, wrapper in self._commandwrappers:
+ extensions.wrapcommand(commands.table, command, wrapper)
+ for cont, funcname, wrapper in self._functionwrappers:
+ extensions.wrapfunction(cont, funcname, wrapper)
+ for c in self._uicallables:
+ c(ui)
+
+ def final_extsetup(self, ui):
+ """Method to be used as a the extension extsetup
+
+ The following operations belong here:
+
+ - Changes depending on the status of other extensions. (if
+ extensions.find('mq'))
+ - Add a global option to all commands
+ - Register revset functions
+ """
+ knownexts = {}
+ for name, symbol in self._revsetsymbols:
+ revset.symbols[name] = symbol
+ for name, kw in self._templatekws:
+ templatekw.keywords[name] = kw
+ for ext, command, wrapper in self._extcommandwrappers:
+ if ext not in knownexts:
+ e = extensions.find(ext)
+ if e is None:
+ raise util.Abort('extension %s not found' % ext)
+ knownexts[ext] = e.cmdtable
+ extensions.wrapcommand(knownexts[ext], commands, wrapper)
+ for c in self._extcallables:
+ c(ui)
+
+ def final_reposetup(self, ui, repo):
+ """Method to be used as a the extension reposetup
+
+ The following operations belong here:
+
+ - All hooks but pre-* and post-*
+ - Modify configuration variables
+ - Changes to repo.__class__, repo.dirstate.__class__
+ """
+ for c in self._repocallables:
+ c(ui, repo)
+
+ def uisetup(self, call):
+ """Decorated function will be executed during uisetup
+
+ example::
+
+ @eh.uisetup
+ def setupbabar(ui):
+ print 'this is uisetup!'
+ """
+ self._uicallables.append(call)
+ return call
+
+ def extsetup(self, call):
+ """Decorated function will be executed during extsetup
+
+ example::
+
+ @eh.extsetup
+ def setupcelestine(ui):
+ print 'this is extsetup!'
+ """
+ self._uicallables.append(call)
+ return call
+
+ def reposetup(self, call):
+ """Decorated function will be executed during reposetup
+
+ example::
+
+ @eh.reposetup
+ def setupzephir(ui, repo):
+ print 'this is reposetup!'
+ """
+ self._repocallables.append(call)
+ return call
+
+ def revset(self, symbolname):
+ """Decorated function is a revset symbol
+
+ The name of the symbol must be given as the decorator argument.
+ The symbol is added during `extsetup`.
+
+ example::
+
+ @eh.revset('hidden')
+ def revsetbabar(repo, subset, x):
+ args = revset.getargs(x, 0, 0, 'babar accept no argument')
+ return [r for r in subset if 'babar' in repo[r].description()]
+ """
+ def dec(symbol):
+ self._revsetsymbols.append((symbolname, symbol))
+ return symbol
+ return dec
+
+
+ def templatekw(self, keywordname):
+ """Decorated function is a revset keyword
+
+ The name of the keyword must be given as the decorator argument.
+ The symbol is added during `extsetup`.
+
+ example::
+
+ @eh.templatekw('babar')
+ def kwbabar(ctx):
+ return 'babar'
+ """
+ def dec(keyword):
+ self._templatekws.append((keywordname, keyword))
+ return keyword
+ return dec
+
+ def wrapcommand(self, command, extension=None):
+ """Decorated function is a command wrapper
+
+ The name of the command must be given as the decorator argument.
+ The wrapping is installed during `uisetup`.
+
+ If the second option `extension` argument is provided, the wrapping
+ will be applied in the extension commandtable. This argument must be a
+ string that will be searched using `extension.find` if not found and
+ Abort error is raised. If the wrapping applies to an extension, it is
+ installed during `extsetup`
+
+ example::
+
+ @eh.wrapcommand('summary')
+ def wrapsummary(orig, ui, repo, *args, **kwargs):
+ ui.note('Barry!')
+ return orig(ui, repo, *args, **kwargs)
+
+ """
+ def dec(wrapper):
+ if extension is None:
+ self._commandwrappers.append((command, wrapper))
+ else:
+ self._extcommandwrappers.append((extension, command, wrapper))
+ return wrapper
+ return dec
+
+ def wrapfunction(self, container, funcname):
+ """Decorated function is a function wrapper
+
+ This function takes two arguments, the container and the name of the
+ function to wrap. The wrapping is performed during `uisetup`.
+ (there is no extension support)
+
+ example::
+
+ @eh.function(discovery, 'checkheads')
+ def wrapfunction(orig, *args, **kwargs):
+ ui.note('His head smashed in and his heart cut out')
+ return orig(*args, **kwargs)
+ """
+ def dec(wrapper):
+ self._functionwrappers.append((container, funcname, wrapper))
+ return wrapper
+ return dec
+
+ def addattr(self, container, funcname):
+ """Decorated function is to be added to the container
+
+ This function takes two arguments, the container and the name of the
+ function to wrap. The wrapping is performed during `uisetup`.
+
+ example::
+
+ @eh.function(context.changectx, 'babar')
+ def babar(ctx):
+ return 'babar' in ctx.description
+ """
+ def dec(func):
+ self._duckpunchers.append((container, funcname, func))
+ return func
+ return dec
+
+eh = exthelper()
+uisetup = eh.final_uisetup
+extsetup = eh.final_extsetup
+reposetup = eh.final_reposetup
+
+#####################################################################
+### Obsolescence Caching Logic ###
+#####################################################################
+
+# Obsolescence related logic can be very slow if we don't have efficient cache.
+#
+# This section implements a cache mechanism that did not make it into core for
+# time reason. It store meaningful set of revision related to obsolescence
+# (obsolete, unstabletble ...
+#
+# Here is:
+#
+# - Computation of meaningful set,
+# - Cache access logic,
+# - Cache invalidation logic,
+# - revset and ctx using this cache.
+#
+
+
+### Computation of meaningful set
+#
+# Most set can be computed with "simple" revset.
+
+#: { set name -> function to compute this set } mapping
+#: function take a single "repo" argument.
+#:
+#: Use the `cachefor` decorator to register new cache function
+cachefuncs = {}
+def cachefor(name):
+ """Decorator to register a function as computing the cache for a set"""
+ def decorator(func):
+ assert name not in cachefuncs
+ cachefuncs[name] = func
+ return func
+ return decorator
+
+@cachefor('obsolete')
+def _computeobsoleteset(repo):
+ """the set of obsolete revisions"""
+ obs = set()
+ nm = repo.changelog.nodemap
+ for prec in repo.obsstore.precursors:
+ rev = nm.get(prec)
+ if rev is not None:
+ obs.add(rev)
+ return set(repo.revs('%ld - public()', obs))
+
+@cachefor('unstable')
+def _computeunstableset(repo):
+ """the set of non obsolete revisions with obsolete parents"""
+ return set(repo.revs('(obsolete()::) - obsolete()'))
+
+@cachefor('suspended')
+def _computesuspendedset(repo):
+ """the set of obsolete parents with non obsolete descendants"""
+ return set(repo.revs('obsolete() and obsolete()::unstable()'))
+
+@cachefor('extinct')
+def _computeextinctset(repo):
+ """the set of obsolete parents without non obsolete descendants"""
+ return set(repo.revs('obsolete() - obsolete()::unstable()'))
+
+@eh.wrapfunction(obsolete.obsstore, '__init__')
+def _initobsstorecache(orig, obsstore, *args, **kwargs):
+ """add a cache attribute to obsstore"""
+ obsstore.caches = {}
+ return orig(obsstore, *args, **kwargs)
+
+### Cache access
+
+def getobscache(repo, name):
+ """Return the set of revision that belong to the <name> set
+
+ Such access may compute the set and cache it for future use"""
+ if not repo.obsstore:
+ return ()
+ if name not in repo.obsstore.caches:
+ repo.obsstore.caches[name] = cachefuncs[name](repo)
+ return repo.obsstore.caches[name]
+
+### Cache clean up
+#
+# To be simple we need to invalidate obsolescence cache when:
+#
+# - new changeset is added:
+# - public phase is changed
+# - obsolescence marker are added
+# - strip is used a repo
+
+
+def clearobscaches(repo):
+ """Remove all obsolescence related cache from a repo
+
+ This remove all cache in obsstore is the obsstore already exist on the
+ repo.
+
+ (We could be smarter here)"""
+ if 'obsstore' in repo._filecache:
+ repo.obsstore.caches.clear()
+
+@eh.wrapfunction(localrepo.localrepository, 'addchangegroup') # new changeset
+@eh.wrapfunction(phases, 'retractboundary') # phase movement
+@eh.wrapfunction(phases, 'advanceboundary') # phase movement
+@eh.wrapfunction(localrepo.localrepository, 'destroyed') # strip
+def wrapclearcache(orig, repo, *args, **kwargs):
+ try:
+ return orig(repo, *args, **kwargs)
+ finally:
+ # we are a bit wide here
+ # we could restrict to:
+ # advanceboundary + phase==public
+ # retractboundary + phase==draft
+ clearobscaches(repo)
+
+@eh.wrapfunction(obsolete.obsstore, 'add') # new marker
+def clearonadd(orig, obsstore, *args, **kwargs):
+ try:
+ return orig(obsstore, *args, **kwargs)
+ finally:
+ obsstore.caches.clear()
+
+### Use the case
+# Function in core that could benefic from the cache are overwritten by cache using version
+
+# changectx method
+
+@eh.addattr(context.changectx, 'unstable')
+def unstable(ctx):
+ """is the changeset unstable (have obsolete ancestor)"""
+ if ctx.node() is None:
+ return False
+ return ctx.rev() in getobscache(ctx._repo, 'unstable')
+
+
+@eh.addattr(context.changectx, 'extinct')
+def extinct(ctx):
+ """is the changeset extinct by other"""
+ if ctx.node() is None:
+ return False
+ return ctx.rev() in getobscache(ctx._repo, 'extinct')
+
+# revset
+
+@eh.revset('obsolete')
+def revsetobsolete(repo, subset, x):
+ """``obsolete()``
+ Changeset is obsolete.
+ """
+ args = revset.getargs(x, 0, 0, 'obsolete takes no argument')
+ obsoletes = getobscache(repo, 'obsolete')
+ return [r for r in subset if r in obsoletes]
+
+@eh.revset('unstable')
+def revsetunstable(repo, subset, x):
+ """``unstable()``
+ Unstable changesets are non-obsolete with obsolete ancestors.
+ """
+ args = revset.getargs(x, 0, 0, 'unstable takes no arguments')
+ unstables = getobscache(repo, 'unstable')
+ return [r for r in subset if r in unstables]
+
+@eh.revset('extinct')
+def revsetextinct(repo, subset, x):
+ """``extinct()``
+ Obsolete changesets with obsolete descendants only.
+ """
+ args = revset.getargs(x, 0, 0, 'extinct takes no arguments')
+ extincts = getobscache(repo, 'extinct')
+ return [r for r in subset if r in extincts]
+
+#####################################################################
+### Complete troubles computation logic ###
+#####################################################################
+
+# there is two kind of trouble not handled by core right now:
+# - latecomer: (successors for public changeset)
+# - conflicting: (two changeset try to succeed to the same precursors)
+#
+# This section add support for those two addition trouble
+#
+# - Cache computation
+# - revset and ctx method
+# - push warning
+
+### Cache computation
+latediff = 1 # flag to prevent taking late comer fix into account
+
+@cachefor('latecomer')
+def _computelatecomerset(repo):
+ """the set of rev trying to obsolete public revision"""
+ candidates = _allsuccessors(repo, repo.revs('public()'),
+ haltonflags=latediff)
+ query = '%ld - obsolete() - public()'
+ return set(repo.revs(query, candidates))
+
+@cachefor('conflicting')
+def _computeconflictingset(repo):
+ """the set of rev trying to obsolete public revision"""
+ conflicting = set()
+ obsstore = repo.obsstore
+ newermap = {}
+ for ctx in repo.set('(not public()) - obsolete()'):
+ prec = obsstore.successors.get(ctx.node(), ())
+ toprocess = set(prec)
+ while toprocess:
+ prec = toprocess.pop()[0]
+ if prec not in newermap:
+ newermap[prec] = newerversion(repo, prec)
+ newer = [n for n in newermap[prec] if n] # filter kill
+ if len(newer) > 1:
+ conflicting.add(ctx.rev())
+ break
+ toprocess.update(obsstore.successors.get(prec, ()))
+ return conflicting
+
+### changectx method
+
+@eh.addattr(context.changectx, 'latecomer')
+def latecomer(ctx):
+ """is the changeset latecomer (Try to succeed to public change)"""
+ if ctx.node() is None:
+ return False
+ return ctx.rev() in getobscache(ctx._repo, 'latecomer')
+
+@eh.addattr(context.changectx, 'conflicting')
+def conflicting(ctx):
+ """is the changeset conflicting (Try to succeed to public change)"""
+ if ctx.node() is None:
+ return False
+ return ctx.rev() in getobscache(ctx._repo, 'conflicting')
+
+### revset symbol
+
+@eh.revset('latecomer')
+def revsetlatecomer(repo, subset, x):
+ """``latecomer()``
+ Changesets marked as successors of public changesets.
+ """
+ args = revset.getargs(x, 0, 0, 'latecomer takes no arguments')
+ lates = getobscache(repo, 'latecomer')
+ return [r for r in subset if r in lates]
+
+@eh.revset('conflicting')
+def revsetconflicting(repo, subset, x):
+ """``conflicting()``
+ Changesets marked as successors of a same changeset.
+ """
+ args = revset.getargs(x, 0, 0, 'conflicting takes no arguments')
+ conf = getobscache(repo, 'conflicting')
+ return [r for r in subset if r in conf]
+
+
+### Discovery wrapping
+
+@eh.wrapfunction(discovery, 'checkheads')
+def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs):
+ """wrap mercurial.discovery.checkheads
+
+ * prevent latecomer and unstable to be pushed
+ """
+ # do not push instability
+ for h in outgoing.missingheads:
+ # Checking heads is enough, obsolete descendants are either
+ # obsolete or unstable.
+ ctx = repo[h]
+ if ctx.latecomer():
+ raise util.Abort(_("push includes a latecomer changeset: %s!")
+ % ctx)
+ if ctx.conflicting():
+ raise util.Abort(_("push includes a conflicting changeset: %s!")
+ % ctx)
+ return orig(repo, remote, outgoing, *args, **kwargs)
+
+#####################################################################
+### Filter extinct changeset from common operation ###
+#####################################################################
+
+@eh.wrapfunction(merge, 'update')
+def wrapmergeupdate(orig, repo, node, *args, **kwargs):
+ """ensure we don't automatically update on hidden changeset"""
+ if node is None:
+ # tip of current branch
+ branch = repo[None].branch()
+ node = repo.revs('last((.:: and branch(%s)) - hidden())', branch)[0]
+ return orig(repo, node, *args, **kwargs)
+
+@eh.wrapfunction(localrepo.localrepository, 'branchtip')
+def obsbranchtip(orig, repo, branch):
+ """ensure "stable" reference does not end on a hidden changeset"""
+ result = ()
+ heads = repo.branchmap().get(branch, ())
+ if heads:
+ result = list(repo.set('last(heads(branch(%n) - hidden()))', heads[0]))
+ if not result:
+ raise error.RepoLookupError(_("unknown branch '%s'") % branch)
+ return result[0].node()
+
+
+#####################################################################
+### Additional Utilities ###
+#####################################################################
+
+# This section contains a lot of small utility function and method
+
+# - Function to create markers
+# - useful alias pstatus and pdiff (should probably go in evolve)
+# - "troubles" method on changectx
+# - function to travel throught the obsolescence graph
+# - function to find useful changeset to stabilize
+
+### Marker Create
+
+def createmarkers(repo, relations, metadata=None, flag=0):
+ """Add obsolete markers between changeset in a repo
+
+ <relations> must be an iterable of (<old>, (<new>, ...)) tuple.
+ `old` and `news` are changectx.
+
+ Current user and date are used except if specified otherwise in the
+ metadata attribute.
+
+ /!\ assume the repo have been locked by the user /!\
+ """
+ # prepare metadata
+ if metadata is None:
+ metadata = {}
+ if 'date' not in metadata:
+ metadata['date'] = '%i %i' % util.makedate()
+ if 'user' not in metadata:
+ metadata['user'] = repo.ui.username()
+ # check future marker
+ tr = repo.transaction('add-obsolescence-marker')
+ try:
+ for prec, sucs in relations:
+ if not prec.mutable():
+ raise util.Abort("Cannot obsolete immutable changeset: %s" % prec)
+ nprec = prec.node()
+ nsucs = tuple(s.node() for s in sucs)
+ if nprec in nsucs:
+ raise util.Abort("Changeset %s cannot obsolete himself" % prec)
+ repo.obsstore.create(tr, nprec, nsucs, flag, metadata)
+ clearobscaches(repo)
+ tr.close()
+ finally:
+ tr.release()
+
+
+### Useful alias
+
+@eh.uisetup
+def _installalias(ui):
+ if ui.config('alias', 'pstatus', None) is None:
+ ui.setconfig('alias', 'pstatus', 'status --rev .^')
+ if ui.config('alias', 'pdiff', None) is None:
+ ui.setconfig('alias', 'pdiff', 'diff --rev .^')
+ if ui.config('alias', 'olog', None) is None:
+ ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden")
+
+# - "troubles" method on changectx
+
+@eh.addattr(context.changectx, 'troubles')
+def troubles(ctx):
+ """Return a tuple listing all the troubles that affect a changeset
+
+ Troubles may be "unstable", "latecomer" or "conflicting".
+ """
+ troubles = []
+ if ctx.unstable():
+ troubles.append('unstable')
+ if ctx.latecomer():
+ troubles.append('latecomer')
+ if ctx.conflicting():
+ troubles.append('conflicting')
+ return tuple(troubles)
+
+### Troubled revset symbol
+
+@eh.revset('troubled')
+def revsetlatecomer(repo, subset, x):
+ """``troubled()``
+ Changesets with troubles.
+ """
+ _ = revset.getargs(x, 0, 0, 'troubled takes no arguments')
+ return list(repo.revs('%ld and (unstable() + latecomer() + conflicting())',
+ subset))
+
+
+### Obsolescence graph
+
+# XXX SOME MAJOR CLEAN UP TO DO HERE XXX
+
+def _precursors(repo, s):
+ """Precursor of a changeset"""
+ cs = set()
+ nm = repo.changelog.nodemap
+ markerbysubj = repo.obsstore.successors
+ for r in s:
+ for p in markerbysubj.get(repo[r].node(), ()):
+ pr = nm.get(p[0])
+ if pr is not None:
+ cs.add(pr)
+ return cs
+
+def _allprecursors(repo, s): # XXX we need a better naming
+ """transitive precursors of a subset"""
+ toproceed = [repo[r].node() for r in s]
+ seen = set()
+ allsubjects = repo.obsstore.successors
+ while toproceed:
+ nc = toproceed.pop()
+ for mark in allsubjects.get(nc, ()):
+ np = mark[0]
+ if np not in seen:
+ seen.add(np)
+ toproceed.append(np)
+ nm = repo.changelog.nodemap
+ cs = set()
+ for p in seen:
+ pr = nm.get(p)
+ if pr is not None:
+ cs.add(pr)
+ return cs
+
+def _successors(repo, s):
+ """Successors of a changeset"""
+ cs = set()
+ nm = repo.changelog.nodemap
+ markerbyobj = repo.obsstore.precursors
+ for r in s:
+ for p in markerbyobj.get(repo[r].node(), ()):
+ for sub in p[1]:
+ sr = nm.get(sub)
+ if sr is not None:
+ cs.add(sr)
+ return cs
+
+def _allsuccessors(repo, s, haltonflags=0): # XXX we need a better naming
+ """transitive successors of a subset
+
+ haltonflags allows to provide flags which prevent the evaluation of a
+ marker. """
+ toproceed = [repo[r].node() for r in s]
+ seen = set()
+ allobjects = repo.obsstore.precursors
+ while toproceed:
+ nc = toproceed.pop()
+ for mark in allobjects.get(nc, ()):
+ if mark[2] & haltonflags:
+ continue
+ for sub in mark[1]:
+ if sub == nullid:
+ continue # should not be here!
+ if sub not in seen:
+ seen.add(sub)
+ toproceed.append(sub)
+ nm = repo.changelog.nodemap
+ cs = set()
+ for s in seen:
+ sr = nm.get(s)
+ if sr is not None:
+ cs.add(sr)
+ return cs
+
+
+
+def newerversion(repo, obs):
+ """Return the newer version of an obsolete changeset"""
+ toproceed = set([(obs,)])
+ # XXX known optimization available
+ newer = set()
+ objectrels = repo.obsstore.precursors
+ while toproceed:
+ current = toproceed.pop()
+ assert len(current) <= 1, 'splitting not handled yet. %r' % current
+ current = [n for n in current if n != nullid]
+ if current:
+ n, = current
+ if n in objectrels:
+ markers = objectrels[n]
+ for mark in markers:
+ toproceed.add(tuple(mark[1]))
+ else:
+ newer.add(tuple(current))
+ else:
+ newer.add(())
+ return sorted(newer)
+
+
+#####################################################################
+### Extending revset and template ###
+#####################################################################
+
+# this section add several useful revset symbol not yet in core.
+# they are subject to changes
+
+### hidden revset is not in core yet
+
+@eh.revset('hidden')
+def revsethidden(repo, subset, x):
+ """``hidden()``
+ Changeset is hidden.
+ """
+ args = revset.getargs(x, 0, 0, 'hidden takes no argument')
+ return [r for r in subset if r in repo.hiddenrevs]
+
+### XXX I'm not sure this revset is useful
+@eh.revset('suspended')
+def revsetsuspended(repo, subset, x):
+ """``suspended()``
+ Obsolete changesets with non-obsolete descendants.
+ """
+ args = revset.getargs(x, 0, 0, 'suspended takes no arguments')
+ suspended = getobscache(repo, 'suspended')
+ return [r for r in subset if r in suspended]
+
+
+@eh.revset('precursors')
+def revsetprecursors(repo, subset, x):
+ """``precursors(set)``
+ Immediate precursors of changesets in set.
+ """
+ s = revset.getset(repo, range(len(repo)), x)
+ cs = _precursors(repo, s)
+ return [r for r in subset if r in cs]
+
+
+@eh.revset('allprecursors')
+def revsetallprecursors(repo, subset, x):
+ """``allprecursors(set)``
+ Transitive precursors of changesets in set.
+ """
+ s = revset.getset(repo, range(len(repo)), x)
+ cs = _allprecursors(repo, s)
+ return [r for r in subset if r in cs]
+
+
+@eh.revset('successors')
+def revsetsuccessors(repo, subset, x):
+ """``successors(set)``
+ Immediate successors of changesets in set.
+ """
+ s = revset.getset(repo, range(len(repo)), x)
+ cs = _successors(repo, s)
+ return [r for r in subset if r in cs]
+
+@eh.revset('allsuccessors')
+def revsetallsuccessors(repo, subset, x):
+ """``allsuccessors(set)``
+ Transitive successors of changesets in set.
+ """
+ s = revset.getset(repo, range(len(repo)), x)
+ cs = _allsuccessors(repo, s)
+ return [r for r in subset if r in cs]
+
+### template keywords
+# XXX it does not handle troubles well :-/
+
+@eh.templatekw('obsolete')
+def obsoletekw(repo, ctx, templ, **args):
+ """:obsolete: String. The obsolescence level of the node, could be
+ ``stable``, ``unstable``, ``suspended`` or ``extinct``.
+ """
+ rev = ctx.rev()
+ if ctx.obsolete():
+ if ctx.extinct():
+ return 'extinct'
+ else:
+ return 'suspended'
+ elif ctx.unstable():
+ return 'unstable'
+ return 'stable'
+
+#####################################################################
+### Various trouble warning ###
+#####################################################################
+
+# This section take care of issue warning to the user when troubles appear
+
+@eh.wrapcommand("update")
+@eh.wrapcommand("pull")
+def wrapmayobsoletewc(origfn, ui, repo, *args, **opts):
+ """Warn that the working directory parent is an obsolete changeset"""
+ res = origfn(ui, repo, *args, **opts)
+ if repo['.'].obsolete():
+ ui.warn(_('Working directory parent is obsolete\n'))
+ return res
+
+# XXX this could wrap transaction code
+# XXX (but this is a bit a layer violation)
+@eh.wrapcommand("commit")
+@eh.wrapcommand("push")
+@eh.wrapcommand("pull")
+@eh.wrapcommand("graft")
+@eh.wrapcommand("phase")
+@eh.wrapcommand("unbundle")
+def warnobserrors(orig, ui, repo, *args, **kwargs):
+ """display warning is the command resulted in more instable changeset"""
+ priorunstables = len(repo.revs('unstable()'))
+ priorlatecomers = len(repo.revs('latecomer()'))
+ priorconflictings = len(repo.revs('conflicting()'))
+ ret = orig(ui, repo, *args, **kwargs)
+ newunstables = len(repo.revs('unstable()')) - priorunstables
+ newlatecomers = len(repo.revs('latecomer()')) - priorlatecomers
+ newconflictings = len(repo.revs('conflicting()')) - priorconflictings
+ if newunstables > 0:
+ ui.warn(_('%i new unstable changesets\n') % newunstables)
+ if newlatecomers > 0:
+ ui.warn(_('%i new latecomer changesets\n') % newlatecomers)
+ if newconflictings > 0:
+ ui.warn(_('%i new conflicting changesets\n') % newconflictings)
+ return ret
+
+@eh.reposetup
+def _repostabilizesetup(ui, repo):
+ """Add a hint for "hg stabilize" when troubles make push fails
+ """
+ if not repo.local():
+ return
+
+ opush = repo.push
+
+ class stabilizerrepo(repo.__class__):
+ def push(self, remote, *args, **opts):
+ """wrapper around pull that pull obsolete relation"""
+ try:
+ result = opush(remote, *args, **opts)
+ except util.Abort, ex:
+ hint = _("use 'hg stabilize' to get a stable history "
+ "or --force to ignore warnings")
+ if (len(ex.args) >= 1
+ and ex.args[0].startswith('push includes ')
+ and ex.hint is None):
+ ex.hint = hint
+ raise
+ return result
+ repo.__class__ = stabilizerrepo
+
+@eh.wrapcommand("summary")
+def obssummary(orig, ui, repo, *args, **kwargs):
+ ret = orig(ui, repo, *args, **kwargs)
+ nbunstable = len(getobscache(repo, 'unstable'))
+ nblatecomer = len(getobscache(repo, 'latecomer'))
+ nbconflicting = len(getobscache(repo, 'unstable'))
+ if nbunstable:
+ ui.write('unstable: %i changesets\n' % nbunstable)
+ else:
+ ui.note('unstable: 0 changesets\n')
+ if nblatecomer:
+ ui.write('latecomer: %i changesets\n' % nblatecomer)
+ else:
+ ui.note('latecomer: 0 changesets\n')
+ if nbconflicting:
+ ui.write('conflicting: %i changesets\n' % nbconflicting)
+ else:
+ ui.note('conflicting: 0 changesets\n')
+ return ret
+
+
+#####################################################################
+### Core Other extension compat ###
+#####################################################################
+
+# This section make official history rewritter create obsolete marker
+
+
+### commit --amend
+# make commit --amend create obsolete marker
+#
+# The precursor is still strip from the repository.
+
+@eh.wrapfunction(cmdutil, 'amend')
+def wrapcmdutilamend(orig, ui, repo, commitfunc, old, *args, **kwargs):
+ oldnode = old.node()
+ new = orig(ui, repo, commitfunc, old, *args, **kwargs)
+ if new != oldnode:
+ lock = repo.lock()
+ try:
+ tr = repo.transaction('post-amend-obst')
+ try:
+ meta = {
+ 'date': '%i %i' % util.makedate(),
+ 'user': ui.username(),
+ }
+ repo.obsstore.create(tr, oldnode, [new], 0, meta)
+ tr.close()
+ clearobscaches(repo)
+ finally:
+ tr.release()
+ finally:
+ lock.release()
+ return new
+
+### rebase
+#
+# - ignore obsolete changeset
+# - create obsolete marker *instead of* striping
+
+def buildstate(orig, repo, dest, rebaseset, *ags, **kws):
+ """wrapper for rebase 's buildstate that exclude obsolete changeset"""
+
+ rebaseset = repo.revs('%ld - extinct()', rebaseset)
+ if not rebaseset:
+ repo.ui.warn(_('whole rebase set is extinct and ignored.\n'))
+ return {}
+ root = min(rebaseset)
+ if not repo._rebasekeep and not repo[root].mutable():
+ raise util.Abort(_("can't rebase immutable changeset %s") % repo[root],
+ hint=_('see hg help phases for details'))
+ return orig(repo, dest, rebaseset, *ags, **kws)
+
+def defineparents(orig, repo, rev, target, state, *args, **kwargs):
+ rebasestate = getattr(repo, '_rebasestate', None)
+ if rebasestate is not None:
+ repo._rebasestate = dict(state)
+ repo._rebasetarget = target
+ return orig(repo, rev, target, state, *args, **kwargs)
+
+def concludenode(orig, repo, rev, p1, *args, **kwargs):
+ """wrapper for rebase 's concludenode that set obsolete relation"""
+ newrev = orig(repo, rev, p1, *args, **kwargs)
+ rebasestate = getattr(repo, '_rebasestate', None)
+ if rebasestate is not None:
+ if newrev is not None:
+ nrev = repo[newrev].rev()
+ else:
+ nrev = p1
+ repo._rebasestate[rev] = nrev
+ return newrev
+
+def cmdrebase(orig, ui, repo, *args, **kwargs):
+
+ reallykeep = kwargs.get('keep', False)
+ kwargs = dict(kwargs)
+ kwargs['keep'] = True
+ repo._rebasekeep = reallykeep
+
+ # We want to mark rebased revision as obsolete and set their
+ # replacements if any. Doing it in concludenode() prevents
+ # aborting the rebase, and is not called with all relevant
+ # revisions in --collapse case. Instead, we try to track the
+ # rebase state structure by sampling/updating it in
+ # defineparents() and concludenode(). The obsolete markers are
+ # added from this state after a successful call.
+ repo._rebasestate = {}
+ repo._rebasetarget = None
+ try:
+ l = repo.lock()
+ try:
+ res = orig(ui, repo, *args, **kwargs)
+ if not reallykeep:
+ # Filter nullmerge or unrebased entries
+ repo._rebasestate = dict(p for p in repo._rebasestate.iteritems()
+ if p[1] >= 0)
+ if not res and not kwargs.get('abort') and repo._rebasestate:
+ # Rebased revisions are assumed to be descendants of
+ # targetrev. If a source revision is mapped to targetrev
+ # or to another rebased revision, it must have been
+ # removed.
+ markers = []
+ if kwargs.get('collapse'):
+ # collapse assume revision disapear because they are all
+ # in the created revision
+ newrevs = set(repo._rebasestate.values())
+ newrevs.remove(repo._rebasetarget)
+ if newrevs:
+ # we create new revision.
+ # A single one by --collapse design
+ assert len(newrevs) == 1
+ new = tuple(repo[n] for n in newrevs)
+ else:
+ # every body died. no new changeset created
+ new = (repo[repo._rebasetarget],)
+ for rev, newrev in sorted(repo._rebasestate.items()):
+ markers.append((repo[rev], new))
+ else:
+ # no collapse assume revision disapear because they are
+ # contained in parent
+ for rev, newrev in sorted(repo._rebasestate.items()):
+ markers.append((repo[rev], (repo[newrev],)))
+ createmarkers(repo, markers)
+ return res
+ finally:
+ l.release()
+ finally:
+ delattr(repo, '_rebasestate')
+ delattr(repo, '_rebasetarget')
+
+@eh.extsetup
+def _rebasewrapping(ui):
+ # warning about more obsolete
+ try:
+ rebase = extensions.find('rebase')
+ if rebase:
+ entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors)
+ extensions.wrapfunction(rebase, 'buildstate', buildstate)
+ extensions.wrapfunction(rebase, 'defineparents', defineparents)
+ extensions.wrapfunction(rebase, 'concludenode', concludenode)
+ extensions.wrapcommand(rebase.cmdtable, "rebase", cmdrebase)
+ except KeyError:
+ pass # rebase not found
+
+
+#####################################################################
+### Old Evolve extension content ###
+#####################################################################
+
+# XXX need clean up and proper sorting in other section
### util function
#############################
@@ -122,8 +1193,7 @@
# add evolution metadata
markers = [(u, (new,)) for u in updates]
markers.append((old, (new,)))
- obsolete = extensions.find('obsolete')
- obsolete.createmarkers(repo, markers)
+ createmarkers(repo, markers)
else:
# newid is an existing revision. It could make sense to
# replace revisions with existing ones but probably not by
@@ -164,14 +1234,13 @@
exc.__class__ = LocalMergeFailure
raise
oldbookmarks = repo.nodebookmarks(nodesrc)
- obsolete = extensions.find('obsolete')
if nodenew is not None:
phases.retractboundary(repo, destphase, [nodenew])
- obsolete.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))])
+ createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))])
for book in oldbookmarks:
repo._bookmarks[book] = nodenew
else:
- obsolete.createmarkers(repo, [(repo[nodesrc], ())])
+ createmarkers(repo, [(repo[nodesrc], ())])
# Behave like rebase, move bookmarks to dest
for book in oldbookmarks:
repo._bookmarks[book] = dest.node()
@@ -297,18 +1366,17 @@
def _solveunstable(ui, repo, orig, dryrun=False):
"""Stabilize a unstable changeset"""
- obsolete = extensions.find('obsolete')
obs = orig.parents()[0]
if not obs.obsolete():
obs = orig.parents()[1]
assert obs.obsolete()
- newer = obsolete.newerversion(repo, obs.node())
+ newer = newerversion(repo, obs.node())
# search of a parent which is not killed
while newer == [()]:
ui.debug("stabilize target %s is plain dead,"
" trying to stabilize on it's parent")
obs = obs.parents()[0]
- newer = obsolete.newerversion(repo, obs.node())
+ newer = newerversion(repo, obs.node())
if len(newer) > 1:
ui.write_err(_("conflict rewriting. can't choose destination\n"))
return 2
@@ -361,7 +1429,6 @@
repo.ui.status(_('atop:'))
if not ui.quiet:
displayer.show(prec)
- obsolete = extensions.find('obsolete')
if dryrun:
todo = 'hg rebase --rev %s --detach %s;\n' % (latecomer, prec.p1())
repo.ui.write(todo)
@@ -386,7 +1453,7 @@
tmpid = relocate(repo, latecomer, prec.p1())
if tmpid is not None:
tmpctx = repo[tmpid]
- obsolete.createmarkers(repo, [(latecomer, (tmpctx,))])
+ createmarkers(repo, [(latecomer, (tmpctx,))])
except MergeFailure:
repo.opener.write('graftstate', latecomer.hex() + '\n')
repo.ui.write_err(_('stabilize failed!\n'))
@@ -427,12 +1494,12 @@
newid = repo.commitctx(new)
if newid is None:
- obsolete.createmarkers(repo, [(tmpctx, ())])
+ createmarkers(repo, [(tmpctx, ())])
newid = prec.node()
else:
phases.retractboundary(repo, latecomer.phase(), [newid])
- obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))],
- flag=obsolete.latediff)
+ createmarkers(repo, [(tmpctx, (repo[newid],))],
+ flag=latediff)
bmupdate(newid)
tr.close()
repo.ui.status(_('commited as %s\n') % node.short(newid))
@@ -446,7 +1513,6 @@
wlock.release()
def _solveconflicting(ui, repo, conflicting, dryrun=False):
- obsolete = extensions.find('obsolete')
base, others = conflictingdata(conflicting)
if len(others) > 1:
raise util.Abort("We do not handle split yet")
@@ -510,7 +1576,7 @@
# no changes
else:
new = repo['.']
- obsolete.createmarkers(repo, [(other, (new,))])
+ createmarkers(repo, [(other, (new,))])
phases.retractboundary(repo, other.phase(), [new.node()])
tr.close()
finally:
@@ -528,9 +1594,8 @@
XXX this woobly function won't survive XXX
"""
- obsolete = extensions.find('obsolete')
for base in ctx._repo.set('reverse(precursors(%d))', ctx):
- newer = obsolete.newerversion(ctx._repo, base.node())
+ newer = newerversion(ctx._repo, base.node())
# drop filter and solution including the original ctx
newer = [n for n in newer if n and ctx.node() not in n]
if newer:
@@ -616,8 +1681,7 @@
markers = []
for n in targetnodes:
markers.append((repo[n], sucs))
- obsolete = extensions.find('obsolete')
- obsolete.createmarkers(repo, markers)
+ createmarkers(repo, markers)
# update to an unkilled parent
wdp = repo['.']
@@ -722,8 +1786,7 @@
# the intermediate revision if any. No need to update
# phases or parents.
if tempid is not None:
- obsolete = extensions.find('obsolete')
- obsolete.createmarkers(repo, [(repo[tempid], ())])
+ createmarkers(repo, [(repo[tempid], ())])
# XXX: need another message in collapse case.
tr.close()
raise error.Abort(_('no updates found'))
@@ -859,8 +1922,7 @@
if newid is None:
raise util.Abort(_('nothing to uncommit'))
# Move local changes on filtered changeset
- obsolete = extensions.find('obsolete')
- obsolete.createmarkers(repo, [(old, (repo[newid],))])
+ createmarkers(repo, [(old, (repo[newid],))])
phases.retractboundary(repo, oldphase, [newid])
repo.dirstate.setparents(newid, node.nullid)
_uncommitdirstate(repo, old, match)
@@ -873,8 +1935,12 @@
finally:
lock.release()
+@eh.wrapcommand('commit')
def commitwrapper(orig, ui, repo, *arg, **kwargs):
- lock = repo.lock()
+ if kwargs.get('amend', False):
+ lock = None
+ else:
+ lock = repo.lock()
try:
obsoleted = kwargs.get('obsolete', [])
if obsoleted:
@@ -883,20 +1949,20 @@
if not result: # commit successed
new = repo['-1']
oldbookmarks = []
- obsolete = extensions.find('obsolete')
markers = []
for old in obsoleted:
oldbookmarks.extend(repo.nodebookmarks(old.node()))
markers.append((old, (new,)))
if markers:
- obsolete.createmarkers(repo, markers)
+ createmarkers(repo, markers)
for book in oldbookmarks:
repo._bookmarks[book] = new.node()
if oldbookmarks:
bookmarks.write(repo)
return result
finally:
- lock.release()
+ if lock is not None:
+ lock.release()
@command('^touch',
[('r', 'rev', [], 'revision to update'),],
@@ -916,7 +1982,6 @@
return 1
if repo.revs('public() and %ld', revs):
raise util.Abort("can't touch public revision")
- obsolete = extensions.find('obsolete')
wlock = repo.wlock()
try:
lock = repo.lock()
@@ -930,7 +1995,7 @@
new, _ = rewrite(repo, ctx, [], ctx,
[ctx.p1().node(), ctx.p2().node()],
commitopts={'extra': extra})
- obsolete.createmarkers(repo, [(ctx, (repo[new],))])
+ createmarkers(repo, [(ctx, (repo[new],))])
phases.retractboundary(repo, ctx.phase(), [new])
if ctx in repo[None].parents():
repo.dirstate.setparents(new, node.nullid)
@@ -966,7 +2031,6 @@
if len(heads) > 1:
raise util.Abort("set have multiple heads")
head = repo[heads[0]]
- obsolete = extensions.find('obsolete')
wlock = repo.wlock()
try:
lock = repo.lock()
@@ -980,7 +2044,7 @@
[root.p1().node(), root.p2().node()],
commitopts={'message': msg})
phases.retractboundary(repo, targetphase, [newid])
- obsolete.createmarkers(repo, [(ctx, (repo[newid],))
+ createmarkers(repo, [(ctx, (repo[newid],))
for ctx in allctx])
tr.close()
finally:
@@ -994,6 +2058,7 @@
wlock.release()
+@eh.wrapcommand('graft')
def graftwrapper(orig, ui, repo, *revs, **kwargs):
kwargs = dict(kwargs)
revs = list(revs) + kwargs.get('rev', [])
@@ -1018,25 +2083,21 @@
finally:
lock.release()
-def extsetup(ui):
- try:
- obsolete = extensions.find('obsolete')
- except KeyError:
- raise error.Abort(_('evolution extension requires obsolete extension.'))
+@eh.extsetup
+def oldevolveextsetup(ui):
try:
rebase = extensions.find('rebase')
except KeyError:
- rebase = None
raise error.Abort(_('evolution extension requires rebase extension.'))
for cmd in ['amend', 'kill', 'uncommit']:
entry = extensions.wrapcommand(cmdtable, cmd,
- obsolete.warnobserrors)
+ warnobserrors)
- entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper)
+ entry = cmdutil.findcmd('commit', commands.table)[1]
entry[1].append(('o', 'obsolete', [],
_("make commit obsolete this revision")))
- entry = extensions.wrapcommand(commands.table, 'graft', graftwrapper)
+ entry = cmdutil.findcmd('graft', commands.table)[1]
entry[1].append(('o', 'obsolete', [],
_("make graft obsoletes this revision")))
entry[1].append(('O', 'old-obsolete', False,