diff --git a/edenscm/mercurial/commands/__init__.py b/edenscm/mercurial/commands/__init__.py index c565e80798..30aadc9e3a 100644 --- a/edenscm/mercurial/commands/__init__.py +++ b/edenscm/mercurial/commands/__init__.py @@ -20,6 +20,7 @@ import time from . import ( debug, debugcheckoutidentifier, + debugmutation, debugstatus, debugstrip, eden, @@ -81,6 +82,7 @@ readonly = registrar.command.readonly table.update(uncommit.cmdtable) table.update(debugcheckoutidentifier.command._table) +table.update(debugmutation.command._table) table.update(debugstatus.command._table) table.update(debugstrip.command._table) table.update(eden.command._table) diff --git a/edenscm/mercurial/commands/debug.py b/edenscm/mercurial/commands/debug.py index ee9001c270..bc40c5cafc 100644 --- a/edenscm/mercurial/commands/debug.py +++ b/edenscm/mercurial/commands/debug.py @@ -3289,45 +3289,3 @@ def debugtreestate(ui, repo, cmd="status", **opts): ) else: raise error.Abort("unrecognised command: %s" % cmd) - - -@command(b"debugmutation", [], _("[REV]")) -def debugmutation(ui, repo, *revs, **opts): - """display the mutation history of a commit""" - repo = repo.unfiltered() - opts = pycompat.byteskwargs(opts) - for rev in scmutil.revrange(repo, revs): - ctx = repo[rev] - nodestack = [[ctx.node()]] - while nodestack: - node = nodestack[-1].pop() - ui.status(("%s%s") % (" " * len(nodestack), hex(node))) - entry = mutation.lookup(repo, node) - if entry is not None: - preds = entry.preds() - mutop = entry.op() - mutuser = entry.user() - mutdate = util.shortdatetime((entry.time(), entry.tz())) - mutsplit = entry.split() or None - origin = entry.origin() - origin = { - None: "", - mutation.ORIGIN_LOCAL: "", - mutation.ORIGIN_COMMIT: " (from remote commit)", - mutation.ORIGIN_OBSMARKER: " (from obsmarker)", - mutation.ORIGIN_SYNTHETIC: " (synthetic)", - }.get(origin, " (unknown origin %s)" % origin) - extra = "" - if mutsplit is not None: - extra += " (split into this and: %s)" % ", ".join( - [hex(n) for n in mutsplit] - ) - ui.status( - (" %s by %s at %s%s%s from:") - % (mutop, mutuser, mutdate, extra, origin) - ) - nodestack.append(list(reversed(preds))) - ui.status(("\n")) - while nodestack and not nodestack[-1]: - nodestack.pop() - return 0 diff --git a/edenscm/mercurial/commands/debugmutation.py b/edenscm/mercurial/commands/debugmutation.py new file mode 100644 index 0000000000..7a16092c7e --- /dev/null +++ b/edenscm/mercurial/commands/debugmutation.py @@ -0,0 +1,185 @@ +# debugmutation.py - command processing for debugmutation* commands +# +# Copyright 2019 Facebook, Inc. +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +from .. import mutation, node as nodemod, pycompat, registrar, scmutil, util +from ..i18n import _ + + +command = registrar.command() + + +@command(b"debugmutation", [], _("[REV]")) +def debugmutation(ui, repo, *revs, **opts): + """display the mutation history of a commit""" + repo = repo.unfiltered() + opts = pycompat.byteskwargs(opts) + for rev in scmutil.revrange(repo, revs): + ctx = repo[rev] + nodestack = [[ctx.node()]] + while nodestack: + node = nodestack[-1].pop() + ui.status(("%s%s") % (" " * len(nodestack), nodemod.hex(node))) + entry = mutation.lookup(repo, node) + if entry is not None: + preds = entry.preds() + mutop = entry.op() + mutuser = entry.user() + mutdate = util.shortdatetime((entry.time(), entry.tz())) + mutsplit = entry.split() or None + origin = entry.origin() + origin = { + None: "", + mutation.ORIGIN_LOCAL: "", + mutation.ORIGIN_COMMIT: " (from remote commit)", + mutation.ORIGIN_OBSMARKER: " (from obsmarker)", + mutation.ORIGIN_SYNTHETIC: " (synthetic)", + }.get(origin, " (unknown origin %s)" % origin) + extra = "" + if mutsplit is not None: + extra += " (split into this and: %s)" % ", ".join( + [nodemod.hex(n) for n in mutsplit] + ) + ui.status( + (" %s by %s at %s%s%s from:") + % (mutop, mutuser, mutdate, extra, origin) + ) + nodestack.append(list(reversed(preds))) + ui.status(("\n")) + while nodestack and not nodestack[-1]: + nodestack.pop() + return 0 + + +@command(b"debugmutationfromobsmarkers", []) +def debugmutationfromobsmarkers(ui, repo, **opts): + """convert obsolescence markers to mutation records""" + obsmarkers = repo.obsstore._all + # Sort obsmarkers by date. Applying them in probable date order gives us + # a better chance of resolving cycles in the right way. + obsmarkers.sort(key=lambda x: x[4]) + newmut = {} + + def checkloopfree(pred, succs): + candidates = {pred} + while candidates: + candidate = candidates.pop() + if candidate in newmut: + mutpreds, mutsplit, _markers = newmut[candidate] + for succ in succs: + if succ in mutpreds: + repo.ui.debug( + "ignoring loop: %s -> %s\n history loops at %s -> %s\n" + % ( + nodemod.hex(pred), + ", ".join([nodemod.hex(s) for s in succs]), + nodemod.hex(succ), + nodemod.hex(candidate), + ) + ) + return False + candidates.update(mutpreds) + return True + + dropprune = 0 + droprevive = 0 + dropundo = 0 + droploop = 0 + dropinvalid = 0 + + for obsmarker in obsmarkers: + obspred, obssuccs, obsflag, obsmeta, obsdate, obsparents = obsmarker + if not obssuccs: + # Skip prune markers + dropprune += 1 + continue + if obssuccs == (obspred,): + # Skip revive markers + droprevive += 1 + continue + obsmeta = dict(obsmeta) + if obsmeta.get("operation") in ("undo", "uncommit", "unamend"): + # Skip undo-style markers + dropundo += 1 + continue + if not checkloopfree(obspred, obssuccs): + # Skip markers that introduce loops + droploop += 1 + continue + if len(obssuccs) > 1: + # Split marker + succ = obssuccs[-1] + if succ in newmut: + preds, split, markers = newmut[succ] + if obsmarker in markers: + # duplicate + continue + repo.ui.debug( + "invalid obsmarker found: %s -> %s is both split and folded\n" + % (nodemod.hex(obspred), nodemod.hex(succ)) + ) + dropinvalid += 1 + continue + newmut[succ] = ([obspred], obssuccs[:-1], [obsmarker]) + elif obssuccs[0] in newmut: + preds, split, markers = newmut[obssuccs[0]] + if obsmarker in markers: + # duplicate + continue + # Fold marker + preds.append(obspred) + markers.append(obsmarker) + else: + # Normal marker + newmut[obssuccs[0]] = ([obspred], None, [obsmarker]) + + repo.ui.debug( + "dropped markers: prune: %s, revive: %s, undo: %s, loop: %s, invalid: %s\n" + % (dropprune, droprevive, dropundo, droploop, dropinvalid) + ) + + entries = [] + + for succ, (preds, split, obsmarkers) in newmut.items(): + if mutation.lookup(repo, succ) is not None: + # Have already converted this successor, or already know about it + continue + mutop = "" + mutuser = "" + mutdate = None + for obsmarker in obsmarkers: + obspred, obssuccs, obsflag, obsmeta, obsdate, obsparents = obsmarker + obsmeta = dict(obsmeta) + obsop = obsmeta.get("operation", "") + if not mutop or obsop not in ("", "copy"): + mutop = obsop + obsuser = obsmeta.get("user", "") + if not mutuser and obsuser: + mutuser = obsuser + if not mutdate: + mutdate = obsdate + + entries.append( + mutation.createsyntheticentry( + repo, + mutation.ORIGIN_OBSMARKER, + preds, + succ, + mutop, + split, + mutuser, + mutdate, + ) + ) + + repo.ui.write( + _("generated %s entries for %s commits\n") % (len(entries), len(newmut)) + ) + with repo.lock(): + count = mutation.recordentries(repo, entries, skipexisting=False) + repo.ui.write(_("wrote %s entries\n") % count) diff --git a/edenscm/mercurial/mutation.py b/edenscm/mercurial/mutation.py index 2eb5dddcbf..75b47933aa 100644 --- a/edenscm/mercurial/mutation.py +++ b/edenscm/mercurial/mutation.py @@ -127,6 +127,7 @@ def createcommitentry(repo, node): def recordentries(repo, entries, skipexisting=True): + count = 0 with repo.transaction("record-mutation") as tr: unfi = repo.unfiltered() ms = repo._mutationstore @@ -137,6 +138,8 @@ def recordentries(repo, entries, skipexisting=True): if succ in unfi or ms.has(succ): continue ms.add(entry) + count += 1 + return count def lookup(repo, node, extra=None): diff --git a/tests/test-completion.t b/tests/test-completion.t index a755ff5127..fa300f4289 100644 --- a/tests/test-completion.t +++ b/tests/test-completion.t @@ -112,6 +112,7 @@ Show debug commands if there are no other candidates debuglocks debugmergestate debugmutation + debugmutationfromobsmarkers debugnamecomplete debugobsolete debugpathcomplete @@ -306,6 +307,7 @@ Show all commands + options debuglocks: force-lock, force-wlock, force-undolog-lock, set-lock, set-wlock debugmergestate: debugmutation: + debugmutationfromobsmarkers: debugnamecomplete: debugobsolete: flags, record-parents, rev, exclusive, index, delete, date, user, template debugpathcomplete: full, normal, added, removed diff --git a/tests/test-help.t b/tests/test-help.t index 1d899ba310..f8805e1917 100644 --- a/tests/test-help.t +++ b/tests/test-help.t @@ -1004,6 +1004,8 @@ Test list of internal help commands print merge state debugmutation display the mutation history of a commit + debugmutationfromobsmarkers + convert obsolescence markers to mutation records debugnamecomplete complete "names" - tags, open branch names, bookmark names debugobsolete