diff -r 95089805c3fc -r 8945a62f9096 hgext/evolve.py --- 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: + # stand for Last Successors Sets + # it contains the list of all last successors for the current node. + lss = [] + for mark in markersfor[node]: + # 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:: + + + + + + + + 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):