sapling/edenscm/mercurial/mutation.py

544 lines
20 KiB
Python
Raw Normal View History

# mutation.py - commit mutation tracking
#
# Copyright 2018 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 error, node as nodemod, util
def record(repo, extra, prednodes, op=None, splitting=None):
for key in "mutpred", "mutuser", "mutdate", "mutop", "mutsplit":
if key in extra:
del extra[key]
if recording(repo):
extra["mutpred"] = ",".join(nodemod.hex(p) for p in prednodes)
extra["mutuser"] = repo.ui.config("mutation", "user") or repo.ui.username()
date = repo.ui.config("mutation", "date")
if date is None:
date = util.makedate()
else:
date = util.parsedate(date)
extra["mutdate"] = "%d %d" % date
if op is not None:
extra["mutop"] = op
if splitting is not None:
extra["mutsplit"] = ",".join(nodemod.hex(n) for n in splitting)
def recording(repo):
return repo.ui.configbool("mutation", "record")
def enabled(repo):
return repo.ui.configbool("mutation", "enabled")
class mutationcache(object):
"""Cache of derived mutation information for a local repo."""
def __init__(self, repo):
self._precomputesuccessorssets(repo)
self._precomputeobsolete(repo)
self._precomputeorphans(repo)
def _precomputesuccessorssets(self, repo):
""""""
unfi = repo.unfiltered()
clrevision = unfi.changelog.changelogrevision
unfimutable = set(unfi.nodes("not public()"))
# successorssets maps mutated commits to the sets of successors. This
# is a map from commit node to lists of successors sets. In the cache
# these are the immediate successors, whether or not they are obsolete.
successorssets = {}
# splitheads maps split destinations to the top of the stack that they
# were split into. The top of the stack contains the split metadata
# and is the real successor of the commit that was split.
splitheads = {}
# contentdivergenceroots is the set of draft commits for which there
# are multiple visible successors sets. The visibile mutable successors
# are "content-divergent".
contentdivergenceroots = set()
# phasedivergenceroots is the set of public commits for which there
# are visible mutable successors. The visible mutable successors are
# "phase-divergent".
phasedivergenceroots = set()
def addsuccs(pred, succs):
succsets = successorssets.setdefault(pred, [])
if succs not in succsets:
succsets.append(succs)
if len(succsets) > 1:
contentdivergenceroots.add(pred)
# Compute successor relationships
for current in unfimutable:
extra = clrevision(current).extra
preds = None
if "mutpred" in extra:
preds = [nodemod.bin(x) for x in extra["mutpred"].split(",")]
split = None
if "mutsplit" in extra:
split = [nodemod.bin(x) for x in extra["mutsplit"].split(",")]
# Compute the full set of successors, this is the current commit,
# plus any commits mentioned in `mutsplit`.
succs = [current]
if split is not None:
for splitnode in split:
# Record that this split successor was a result of this
# split operation by linking it to the current commit.
splitheads[splitnode] = current
succs = split + succs
# Now add `succs` as a successor set for all predecessors.
if preds is not None:
for pred in preds:
addsuccs(pred, succs)
if pred in unfi and pred not in unfimutable:
# We have traversed back to a public (immutable) commit.
# This means its successors might be phase divergent, so
# mark the public commit as a phase divergence root.
phasedivergenceroots.add(pred)
# ``successorssets`` is a map from a mutated commit to the sets of
# commits that immediately replace it.
self._successorssets = successorssets
# ``splitheads`` is a map of commits that were created by splitting
# another commit to the top of the stack that they were split into.
# The top-of-stack commit contains the mutation record.
self._splitheads = splitheads
# ``phasedivergenceroots`` is a set of public commits that have visible
# draft successors. The successors are all "phase divergent".
self._phasedivergenceroots = phasedivergenceroots
# ``contentdivergenceroots`` is a set of draft commits that have
# multiple visible eventual successors sets. These eventual successors
# sets are all "content divergent".
self._contentdivergenceroots = contentdivergenceroots
def _precomputeobsolete(self, repo):
successorssets = self._successorssets
# Compute obsolete commits by traversing the commit graph looking for
# commits that have a visible or obsolete successor.
obsolete = set()
for current in repo.nodes("not public()"):
thislevel = {current}
nextlevel = set()
seen = set()
while thislevel:
for node in thislevel:
if node in seen:
continue
seen.add(node)
for succset in successorssets.get(node, ()):
nextlevel.update(succset)
# This node is obsolete if any successor is visible in the repo.
# If any successor is already known to be obsolete, we can also
# assume that the current node is obsolete without checking
# further.
if any(
nextnode in obsolete or nextnode in repo for nextnode in nextlevel
):
obsolete.add(current)
break
thislevel = nextlevel
nextlevel = set()
# ``obsolete`` is the set of all visible commits that have been mutated
# (i.e., have a visible successor).
self._obsolete = obsolete
def _precomputeorphans(self, repo):
obsolete = self._obsolete
unfi = repo.unfiltered()
clparents = unfi.changelog.parents
mutable = set(repo.nodes("not public()"))
# Compute orphaned and extinct commits by traversing the commit graph looking for
# obsolete commits.
#
# Orphaned commits are equivalent to `obsolete():: - obsolete()`, and
# extinct commits are equivalent to `obsolete() - ::orphan()`,
# except that these won't perform well until we have a fast child
# look-up.
orphan = set()
extinct = set(obsolete)
for head in repo.nodes("heads(not public())"):
stack = [head]
visited = [0]
# True if all commits up to this point are obsolete.
allobsolete = [head in obsolete]
# Stack index of the most recent obsolete commit, or -1 if none are.
lastobsolete = [0 if head in obsolete else -1]
while stack:
current = stack[-1]
isobsolete = current in obsolete
if visited[-1] == 0:
if isobsolete:
orphan.update(stack[lastobsolete[-1] + 1 : -1])
if not allobsolete[-1]:
extinct.discard(current)
if visited[-1] < 2:
parent = clparents(current)[visited[-1]]
visited[-1] += 1
if parent != nodemod.nullid and parent in mutable:
lastobsolete.append(
len(stack) - 1 if isobsolete else lastobsolete[-1]
)
stack.append(parent)
allobsolete.append(allobsolete[-1] and isobsolete)
visited.append(0)
else:
stack.pop()
allobsolete.pop()
lastobsolete.pop()
visited.pop()
# ``orphan`` is the set of all visible but not obsolete commits that
# have an obsolete ancestor.
self._orphan = orphan
# ``extinct`` is the set of all obsolete commits that do not have any
# orphaned descendants.
self._extinct = extinct
def allpredecessors(repo, nodes, startdepth=None, stopdepth=None):
"""Yields all the nodes that are predecessors of the given nodes.
Some predecessors may not be known locally."""
unfi = repo.unfiltered()
mc = repo._mutationcache
cl = unfi.changelog
clrev = cl.changelogrevision
depth = 0
thislevel = set(nodes)
nextlevel = set()
seen = set()
while thislevel and (stopdepth is None or depth < stopdepth):
for current in thislevel:
if current in seen:
continue
seen.add(current)
if startdepth is None or depth >= startdepth:
yield current
mainnode = mc._splitheads.get(current, current)
if mainnode in unfi:
extra = clrev(mainnode).extra
pred = None
if "mutpred" in extra:
pred = [nodemod.bin(x) for x in extra["mutpred"].split(",")]
else:
continue
if pred is not None:
for nextnode in pred:
if nextnode not in seen:
nextlevel.add(nextnode)
depth += 1
thislevel = nextlevel
nextlevel = set()
def allsuccessors(repo, nodes, startdepth=None, stopdepth=None):
"""Yields all the nodes that are successors of the given nodes.
Successors that are not known locally may be omitted."""
mc = repo._mutationcache
depth = 0
thislevel = set(nodes)
nextlevel = set()
seen = set()
while thislevel and (stopdepth is None or depth < stopdepth):
for current in thislevel:
if current in seen:
continue
seen.add(current)
if startdepth is None or depth >= startdepth:
yield current
for succset in mc._successorssets.get(current, ()):
nextlevel.update(succset)
depth += 1
thislevel = nextlevel
nextlevel = set()
def obsoletenodes(repo):
return repo._mutationcache._obsolete
def extinctnodes(repo):
return repo._mutationcache._extinct
def orphannodes(repo):
return repo._mutationcache._orphan
def phasedivergentnodes(repo):
return (
n
for n in allsuccessors(
repo, repo._mutationcache._phasedivergenceroots, startdepth=1
)
if n in repo
)
def contentdivergentnodes(repo):
return (
n
for n in allsuccessors(
repo, repo._mutationcache._contentdivergenceroots, startdepth=1
)
if n in repo
)
def predecessorsset(repo, startnode, closest=False):
"""Return a list of the commits that were replaced by the startnode.
If there are no such commits, returns a list containing the startnode.
If ``closest`` is True, returns a list of the visible commits that are the
closest previous version of the start node.
If ``closest`` is False, returns a list of the earliest original versions of
the start node.
"""
unfi = repo.unfiltered()
cl = unfi.changelog
clrevision = cl.changelogrevision
def get(node):
if node in unfi:
extra = clrevision(node).extra
if "mutpred" in extra:
return [nodemod.bin(x) for x in extra["mutpred"].split(",")]
return [node]
preds = [startnode]
nextpreds = sum((get(p) for p in preds), [])
expanded = nextpreds != preds
while expanded:
if all(p in repo for p in nextpreds):
# We have found a set of predecessors that are all visible - this is
# a valid set to return.
preds = nextpreds
if closest:
break
# Now look at the next predecessors of each commit.
newnextpreds = sum((get(p) for p in nextpreds), [])
else:
# Expand out to the predecessors of the commits until we find visible
# ones.
newnextpreds = sum(([p] if p in repo else get(p) for p in nextpreds), [])
expanded = newnextpreds != nextpreds
nextpreds = newnextpreds
if not expanded:
# We've reached a stable state and some of the commits might not be
# visible. Remove the invisible commits, and continue with what's
# left.
newnextpreds = [p for p in nextpreds if p in repo]
if newnextpreds:
expanded = newnextpreds != nextpreds
nextpreds = newnextpreds
return util.removeduplicates(preds)
def _succproduct(succsetlist):
"""Takes a list of successorsset lists and returns a single successorsset
list representing the cartesian product of those successorsset lists.
The ordering of elements within the lists must be preserved.
>>> _succproduct([[[1]], [[2]]])
[[1, 2]]
>>> _succproduct([[[1, 2]], [[3, 4]]])
[[1, 2, 3, 4]]
>>> _succproduct([[[1, 2], [3, 4]], [[5, 6]]])
[[1, 2, 5, 6], [3, 4, 5, 6]]
>>> _succproduct([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
[[1, 2, 5, 6], [3, 4, 5, 6], [1, 2, 7, 8], [3, 4, 7, 8]]
>>> _succproduct([[[1, 2], [3, 4]], [[2, 3], [7, 8]]])
[[1, 2, 3], [3, 4, 2], [1, 2, 7, 8], [3, 4, 7, 8]]
>>> _succproduct([[[1, 2], [3, 4]], [[1, 2], [7, 8]]])
[[1, 2], [3, 4, 1, 2], [1, 2, 7, 8], [3, 4, 7, 8]]
>>> _succproduct([[[1], [2]], [[3], [4]]])
[[1, 3], [2, 3], [1, 4], [2, 4]]
>>> _succproduct([[[5]], [[4]], [[3]], [[2]], [[1]]])
[[5, 4, 3, 2, 1]]
"""
# Start with the first successorsset.
product = succsetlist[0]
for succsets in succsetlist[1:]:
# For each of the remaining successorssets, compute the product with
# the successorsset so far.
newproduct = []
for succset in succsets:
for p in product:
newproduct.append(p + [s for s in succset if s not in p])
product = newproduct
return product
def successorssets(repo, startnode, closest=False, cache=None):
"""Return a list of lists of commits that replace the startnode.
If there are no such commits, returns a list containing a single list
containing the startnode.
If ``closest`` is True, the lists contain the visible commits that are the
closest next version of the start node.
If ``closest`` is False, the lists contain the latest versions of the start
node.
The ``cache`` parameter is unused. It is provided to make this function
signature-compatible with ``obsutil.successorssets``.
"""
mc = repo._mutationcache
def getsets(node):
return sorted(mc._successorssets.get(node, [[node]]))
succsets = [[startnode]]
nextsuccsets = getsets(startnode)
expanded = nextsuccsets != succsets
while expanded:
if all(s in repo for succset in nextsuccsets for s in succset):
# We have found a set of successor sets that all contain visible
# commits - this is a valid set to return.
succsets = nextsuccsets
if closest:
break
# Now look at the next successors of each successors set. When
# commits are modified in different ways (i.e. they have been
# diverged), we need to find all possible permutations that replace
# the original nodes. To do this, we compute the cartesian product
# of the successors sets of each successor in the original
# successors set.
#
# For example, if A is split into B and C, B is diverged to B1 and
# B2, and C is also diverged to C1 and C2, then the successors sets
# of A are: [B1, C1], [B1, C2], [B2, C1], [B2, C2], which is the
# cartesian product: [B1, B2] x [C1, C2].
newnextsuccsets = sum(
[
_succproduct([getsets(succ) for succ in succset])
for succset in nextsuccsets
],
[],
)
else:
# Expand each successors set out to its successors until we find
# visible commit. Again, use the cartesian product to find all
# permutations.
newnextsuccsets = sum(
[
_succproduct(
[
[[succ]] if succ in repo else getsets(succ)
for succ in succset
]
)
for succset in nextsuccsets
],
[],
)
expanded = newnextsuccsets != nextsuccsets
nextsuccsets = newnextsuccsets
if not expanded:
# We've reached a stable state and some of the commits might not be
# visible. Remove the invisible commits, and continue with what's
# left.
newnextsuccsets = [
[s for s in succset if s in repo] for succset in nextsuccsets
]
# Remove sets that are now empty.
newnextsuccsets = [succset for succset in newnextsuccsets if succset]
if newnextsuccsets:
expanded = newnextsuccsets != nextsuccsets
nextsuccsets = newnextsuccsets
return util.removeduplicates(succsets, key=frozenset)
def foreground(repo, nodes):
"""Returns all nodes in the "foreground" of the given nodes.
The foreground of a commit is the transitive closure of all descendants
and successors of the commit.
"""
unfi = repo.unfiltered()
foreground = set()
newctxs = set(unfi.set("%ln::", nodes))
while newctxs:
newnodes = set(c.node() for c in newctxs) - foreground
newnodes.update(allsuccessors(repo, newnodes))
foreground = foreground | newnodes
newctxs = set(unfi.set("(%ln::) - (%ln)", newnodes, newnodes))
return foreground
def toposortrevs(repo, revs, predmap):
"""topologically sort revs according to the given predecessor map"""
dag = {}
valid = set(revs)
heads = set(revs)
clparentrevs = repo.changelog.parentrevs
for rev in revs:
prev = [p for p in clparentrevs(rev) if p in valid]
prev.extend(predmap[rev])
heads.difference_update(prev)
dag[rev] = prev
if not heads:
raise error.Abort("commit predecessors and ancestors contain a cycle")
seen = set()
sortedrevs = []
revstack = list(reversed(sorted(heads)))
while revstack:
rev = revstack[-1]
if rev not in seen:
seen.add(rev)
for next in reversed(dag[rev]):
if next not in seen:
revstack.append(next)
else:
sortedrevs.append(rev)
revstack.pop()
return sortedrevs
def toposort(repo, items, nodefn=None):
"""topologically sort nodes according to the given predecessor map
items can either be nodes, or something convertible to nodes by a provided
node function.
"""
if nodefn is None:
nodefn = lambda item: item
clrev = repo.changelog.rev
revmap = {clrev(nodefn(x)): i for i, x in enumerate(items)}
predmap = {}
for item in items:
node = nodefn(item)
rev = clrev(node)
predmap[rev] = [
r
for r in map(clrev, predecessorsset(repo, node, closest=True))
if r != rev and r in revmap
]
sortedrevs = toposortrevs(repo, revmap.keys(), predmap)
return [items[revmap[r]] for r in sortedrevs]