--- a/hgext/evolve.py Sun Oct 14 16:23:25 2012 +0200
+++ b/hgext/evolve.py Tue Oct 23 16:53:11 2012 +0200
@@ -19,6 +19,10 @@
- improves some aspect of the early implementation in 2.3
'''
+testedwith = '2.3 2.3.1 2.3.2'
+buglink = 'https://bitbucket.org/marmoute/mutable-history/issues'
+
+
import random
from mercurial import util
@@ -28,7 +32,16 @@
if not obsolete._enabled:
obsolete._enabled = True
except ImportError:
- raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)')
+ raise util.Abort('Evolve extension requires Mercurial 2.3 (or later)')
+
+try:
+ getattr(obsolete, 'getrevs') # 2.4 specific
+ raise util.Abort('Your version of Mercurial is too recent for this '
+ 'version of evolve',
+ hint="upgrade your evolve")
+except AttributeError:
+ pass
+
from mercurial import bookmarks
from mercurial import cmdutil
@@ -324,7 +337,7 @@
# 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)
+# - divergent: (two changeset try to succeed to the same precursors)
#
# This section add support for those two addition trouble
#
@@ -343,25 +356,25 @@
query = '%ld - obsolete() - public()'
return set(repo.revs(query, candidates))
-@cachefor('conflicting')
-def _computeconflictingset(repo):
+@cachefor('divergent')
+def _computedivergentset(repo):
"""the set of rev trying to obsolete public revision"""
- conflicting = set()
+ divergent = set()
obsstore = repo.obsstore
newermap = {}
for ctx in repo.set('(not public()) - obsolete()'):
- prec = obsstore.successors.get(ctx.node(), ())
- toprocess = set(prec)
+ mark = obsstore.successors.get(ctx.node(), ())
+ toprocess = set(mark)
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
+ successorssets(repo, prec, newermap)
+ newer = [n for n in newermap[prec] if n]
if len(newer) > 1:
- conflicting.add(ctx.rev())
+ divergent.add(ctx.rev())
break
- toprocess.update(obsstore.successors.get(prec, ()))
- return conflicting
+ toprocess.update(obsstore.successors.get(prec, ()))
+ return divergent
### changectx method
@@ -373,11 +386,12 @@
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)"""
+@eh.addattr(context.changectx, 'divergent')
+def divergent(ctx):
+ """is the changeset divergent (Try to succeed to public change)"""
if ctx.node() is None:
return False
- return ctx.rev() in getobscache(ctx._repo, 'conflicting')
+ return ctx.rev() in getobscache(ctx._repo, 'divergent')
### revset symbol
@@ -391,24 +405,17 @@
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]
-
@eh.revset('divergent')
def revsetdivergent(repo, subset, x):
"""``divergent()``
Changesets marked as successors of a same changeset.
"""
args = revset.getargs(x, 0, 0, 'divergent takes no arguments')
- conf = getobscache(repo, 'conflicting')
+ conf = getobscache(repo, 'divergent')
return [r for r in subset if r in conf]
+
### Discovery wrapping
@eh.wrapfunction(discovery, 'checkheads')
@@ -425,8 +432,8 @@
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!")
+ if ctx.divergent():
+ raise util.Abort(_("push includes a divergent changeset: %s!")
% ctx)
return orig(repo, remote, outgoing, *args, **kwargs)
@@ -498,15 +505,15 @@
def troubles(ctx):
"""Return a tuple listing all the troubles that affect a changeset
- Troubles may be "unstable", "latecomer" or "conflicting".
+ Troubles may be "unstable", "latecomer" or "divergent".
"""
troubles = []
if ctx.unstable():
troubles.append('unstable')
if ctx.latecomer():
troubles.append('latecomer')
- if ctx.conflicting():
- troubles.append('conflicting')
+ if ctx.divergent():
+ troubles.append('divergent')
return tuple(troubles)
### Troubled revset symbol
@@ -517,7 +524,7 @@
Changesets with troubles.
"""
_ = revset.getargs(x, 0, 0, 'troubled takes no arguments')
- return repo.revs('%ld and (unstable() + latecomer() + conflicting())',
+ return repo.revs('%ld and (unstable() + latecomer() + divergent())',
subset)
@@ -597,29 +604,123 @@
cs.add(sr)
return cs
+nodemod = node
+def successorssets(repo, initialnode, cache=None):
+ """Return the newer version of an obsolete changeset"""
+
+ # prec -> markers mapping
+ markersfor = repo.obsstore.precursors
+
+ # Stack of node need to know the last successors set
+ toproceed = [initialnode]
+ # set version of toproceed for fast loop detection
+ stackedset = set(toproceed)
+ if cache is None:
+ cache = {}
+ while toproceed:
+ # work on the last node of the stack
+ node = toproceed[-1]
+ if node in cache:
+ # We already have a value for it.
+ # Keep working on something else.
+ stackedset.remove(toproceed.pop())
+ elif node not in markersfor:
+ # The node is not obsolete.
+ # This mean it is its own last successors.
+ if node in repo:
+ # We have a valid last successors.
+ cache[node] = [(node,)]
+ else:
+ # final obsolete version is unknown locally.
+ # Do not count that as a valid successors
+ cache[node] = []
+ else:
+ # <lss> stand for Last Successors Sets
+ # it contains the list of all last successors for the current node.
+ lss = []
+ for mark in markersfor[node]:
+ # <mlss> stand for Marker Last Successors Sets
+ # it contains the list of last successors set introduced by
+ # this marker.
+ mlss = [[]]
+ # iterate over possible multiple successors
+ for suc in mark[1]:
+ if suc not in cache:
+ # We do not know the last successors of that yet.
+ if suc in stackedset:
+ # Loop detected!
+ #
+ # we won't be able to ever compute a proper last
+ # successors the naive and simple approve is to
+ # consider it killed
+ cache[suc] = []
+ else:
+ # Add the successor to the stack and break the next
+ # iteration will work on this successors and the
+ # algorithm will eventually process the current
+ # node again.
+ toproceed.append(suc)
+ stackedset.add(suc)
+ break
+ # if we did not break, we can extend the possible set of
+ # last successors.
+ #
+ # I say "extends" because if the marker have multiple
+ # successors we have to generate
+ #
+ # if successors have multiple successors set (when ther are
+ # divergent themself), we do a cartesian product of
+ # possible successors set of already processed successors
+ # and newly obtains successors set.
+ newmlss = []
+ for prefix in mlss:
+ for suffix in cache[suc]:
+ newss = list(prefix)
+ for part in suffix:
+ # do not duplicated entry in successors set.
+ # first entry win.
+ if part not in newss:
+ newss.append(part)
+ newmlss.append(newss)
+ mlss = newmlss
+ else:
+ # note: mlss is still empty if the marker was a bare killing
+ # of this changeset
+ #
+ # We extends the list of all possible successors sets with
+ # successors set continuted by this marker
+ lss.extend(mlss)
+ # we use continue here to skip the break right bellow
+ continue
+ # propagate "nested for" break.
+ # if the nested for exited on break, it did not ran the else
+ # clause and didn't "continue
+ break
+ else:
+ # computation was succesful for *all* marker.
+ # Add computed successors set to the cache
+ # (will be poped from to proceeed) on the new iteration
+ #
+ # We remove successors set that are subset of another one
+ # this fil
+ candsucset = sorted(((len(ss), set(ss), ss) for ss in lss),
+ reverse=True)
+ finalsucset = []
+ for cl, cs, css in candsucset:
+ if not css:
+ # remove empty successors set
+ continue
+ for fs, fss in finalsucset:
+ if cs.issubset(fs):
+ break
+ else:
+ finalsucset.append((cs, css))
+ finalsucset = [s[1] for s in finalsucset]
+ finalsucset.reverse()
+ cache[node] = finalsucset
+ return cache[initialnode]
-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)
#####################################################################
@@ -724,19 +825,19 @@
"""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()'))
+ priordivergents = len(repo.revs('divergent()'))
ret = orig(ui, repo, *args, **kwargs)
# workaround phase stupidity
phases._filterunknown(ui, repo.changelog, repo._phasecache.phaseroots)
newunstables = len(repo.revs('unstable()')) - priorunstables
newlatecomers = len(repo.revs('latecomer()')) - priorlatecomers
- newconflictings = len(repo.revs('conflicting()')) - priorconflictings
+ newdivergents = len(repo.revs('divergent()')) - priordivergents
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)
+ if newdivergents > 0:
+ ui.warn(_('%i new divergent changesets\n') % newdivergents)
return ret
@eh.reposetup
@@ -776,10 +877,10 @@
ret = orig(ui, repo, *args, **kwargs)
nbunstable = len(getobscache(repo, 'unstable'))
nblatecomer = len(getobscache(repo, 'latecomer'))
- nbconflicting = len(getobscache(repo, 'unstable'))
+ nbdivergent = len(getobscache(repo, 'unstable'))
write('unstable: %i changesets\n', nbunstable)
write('latecomer: %i changesets\n', nblatecomer)
- write('conflicting: %i changesets\n', nbconflicting)
+ write('divergent: %i changesets\n', nbdivergent)
return ret
@@ -998,7 +1099,7 @@
- rebase unstable changeset to make it stable again,
- create proper diff from latecomer changeset,
- - merge conflicting changeset.
+ - merge divergent changeset.
By default, take the first troubles changeset that looks relevant.
@@ -1008,7 +1109,7 @@
working directory parent revision or one of its descendants and rebase
it.
- - For conflicting this mean "." if applicable.
+ - For divergent this mean "." if applicable.
With --any, evolve pick any troubled changeset to solve
@@ -1041,8 +1142,8 @@
return _solveunstable(ui, repo, tr, opts['dry_run'])
elif 'latecomer' in troubles:
return _solvelatecomer(ui, repo, tr, opts['dry_run'])
- elif 'conflicting' in troubles:
- return _solveconflicting(ui, repo, tr, opts['dry_run'])
+ elif 'divergent' in troubles:
+ return _solvedivergent(ui, repo, tr, opts['dry_run'])
else:
assert False # WHAT? unknown troubles
@@ -1051,14 +1152,14 @@
tr = _stabilizableunstable(repo, repo['.'])
if tr is None:
wdp = repo['.']
- if 'conflicting' in wdp.troubles():
+ if 'divergent' in wdp.troubles():
tr = wdp
if tr is None and pickany:
troubled = list(repo.set('unstable()'))
if not troubled:
troubled = list(repo.set('latecomer()'))
if not troubled:
- troubled = list(repo.set('conflicting()'))
+ troubled = list(repo.set('divergent()'))
if troubled:
tr = troubled[0]
@@ -1089,13 +1190,13 @@
if not obs.obsolete():
obs = orig.parents()[1]
assert obs.obsolete()
- newer = newerversion(repo, obs.node())
+ newer = successorssets(repo, obs.node())
# search of a parent which is not killed
- while newer == [()]:
+ while not newer or newer == [()]:
ui.debug("stabilize target %s is plain dead,"
" trying to stabilize on its parent")
obs = obs.parents()[0]
- newer = newerversion(repo, obs.node())
+ newer = successorssets(repo, obs.node())
if len(newer) > 1:
ui.write_err(_("conflict rewriting. can't choose destination\n"))
return 2
@@ -1231,22 +1332,22 @@
finally:
wlock.release()
-def _solveconflicting(ui, repo, conflicting, dryrun=False):
- base, others = conflictingdata(conflicting)
+def _solvedivergent(ui, repo, divergent, dryrun=False):
+ base, others = divergentdata(divergent)
if len(others) > 1:
raise util.Abort("We do not handle split yet")
other = others[0]
- if conflicting.phase() <= phases.public:
+ if divergent.phase() <= phases.public:
raise util.Abort("We can't resolve this conflict from the public side")
if len(other.parents()) > 1:
- raise util.Abort("conflicting changeset can't be a merge (yet)")
- if other.p1() not in conflicting.parents():
+ raise util.Abort("divergent changeset can't be a merge (yet)")
+ if other.p1() not in divergent.parents():
raise util.Abort("parents are not common (not handled yet)")
displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate})
ui.status(_('merge:'))
if not ui.quiet:
- displayer.show(conflicting)
+ displayer.show(divergent)
ui.status(_('with: '))
if not ui.quiet:
displayer.show(other)
@@ -1254,23 +1355,23 @@
if not ui.quiet:
displayer.show(base)
if dryrun:
- ui.write('hg update -c %s &&\n' % conflicting)
+ ui.write('hg update -c %s &&\n' % divergent)
ui.write('hg merge %s &&\n' % other)
ui.write('hg commit -m "auto merge resolving conflict between '
- '%s and %s"&&\n' % (conflicting, other))
+ '%s and %s"&&\n' % (divergent, other))
ui.write('hg up -C %s &&\n' % base)
ui.write('hg revert --all --rev tip &&\n')
ui.write('hg commit -m "`hg log -r %s --template={desc}`";\n'
- % conflicting)
+ % divergent)
return
wlock = lock = None
try:
wlock = repo.wlock()
lock = repo.lock()
- if conflicting not in repo[None].parents():
+ if divergent not in repo[None].parents():
repo.ui.status(_('updating to "local" conflict\n'))
- hg.update(repo, conflicting.rev())
- repo.ui.note(_('merging conflicting changeset\n'))
+ hg.update(repo, divergent.rev())
+ repo.ui.note(_('merging divergent changeset\n'))
stats = merge.update(repo,
other.node(),
branchmerge=True,
@@ -1291,13 +1392,13 @@
/!\ * hg ci -m "same message as the amended changeset" => new cset Y
/!\ * hg kill -n Y W Z
""")
- tr = repo.transaction('stabilize-conflicting')
+ tr = repo.transaction('stabilize-divergent')
try:
- repo.dirstate.setparents(conflicting.node(), node.nullid)
+ repo.dirstate.setparents(divergent.node(), node.nullid)
oldlen = len(repo)
amend(ui, repo)
if oldlen == len(repo):
- new = conflicting
+ new = divergent
# no changes
else:
new = repo['.']
@@ -1310,7 +1411,7 @@
lockmod.release(lock, wlock)
-def conflictingdata(ctx):
+def divergentdata(ctx):
"""return base, other part of a conflict
This only return the first one.
@@ -1318,7 +1419,7 @@
XXX this woobly function won't survive XXX
"""
for base in ctx._repo.set('reverse(precursors(%d))', ctx):
- newer = newerversion(ctx._repo, base.node())
+ newer = successorssets(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:
@@ -1778,6 +1879,60 @@
finally:
lockmod.release(lock, wlock)
+if 'debugsuccessorssets' not in commands.table:
+
+ @command('debugsuccessorssets',
+ [],
+ _('[REV]'))
+ def debugsuccessorssets(ui, repo, *revs):
+ """show set of successors for revision
+
+ Successors set of changeset A are a consistent group of revision that
+ succeed to A. Successors set contains non-obsolete changeset only.
+
+ In most case a changeset A have zero (changeset pruned) or a single
+ successors set that contains a single successors (changeset A replacement
+ by A')
+
+ But splitted changeset will result with successors set containing more than
+ a single element. Divergent rewritting will result in multiple successor
+ set.
+
+ result is displayed as follows::
+
+ <rev1>
+ <successors-1A>
+ <rev2>
+ <successors-2A>
+ <successors-2B1> <successors-2B1> <successors-2B1>
+
+ here rev2 have two possible successors sets. One hold three elements.
+
+ add --debug if you want full size node id.
+ """
+ cache = {}
+ s = str
+ if ui.debug:
+ def s(ctx):
+ return ctx.hex()
+ for rev in scmutil.revrange(repo, revs):
+ ctx = repo[rev]
+ if ui.debug():
+ ui.write('%s\n'% ctx.hex())
+ s = node.hex
+ else:
+ ui.write('%s\n'% ctx)
+ s = node.short
+ for ss in successorssets(repo, ctx.node(), cache):
+ if ss:
+ ui.write(' ')
+ ui.write(s(ss[0]))
+ for n in ss[1:]:
+ ui.write(' ')
+ ui.write(s(n))
+ ui.write('\n')
+ pass
+
@eh.wrapcommand('graft')
def graftwrapper(orig, ui, repo, *revs, **kwargs):