merge: split update() into goto() and merge()

Summary:
merge.update() had a lot of complicated arg checking to differentiate simple working copy updates from tricker three way merges. In particular, the eden and nativeupdate code paths only support the simple update case.

To make things clearer, let's separate update() into goto() and merge(). goto() handles the simple case, and moving it to Rust will be the (achievable) goal of some upcoming work. merge() handles the full enchilada, and isn't ripe for Rustification at this point.

Reviewed By: zzl0

Differential Revision: D49058005

fbshipit-source-id: f8106c04c48bfba0e7fad244e1dd9933142674e1
This commit is contained in:
Muir Manders 2023-10-05 08:03:08 -07:00 committed by Facebook GitHub Bot
parent 5144e52f36
commit b22ca6b910
13 changed files with 178 additions and 150 deletions

View File

@ -784,13 +784,11 @@ def _dobackout(ui, repo, node=None, rev=None, **opts):
dsguard = dirstateguard.dirstateguard(repo, "backout")
try:
ui.setconfig("ui", "forcemerge", opts.get("tool", ""), "backout")
stats = mergemod.update(
repo, parent, branchmerge=True, force=True, ancestor=node
)
stats = mergemod.merge(repo, parent, force=True, ancestor=node)
repo.setparents(op1, op2)
# Ensure reverse-renames are preserved during the backout. In theory
# merge.update() should handle this, but it's extremely complex, so
# merge.merge() should handle this, but it's extremely complex, so
# let's just double check it here.
_replayrenames(repo, node)

View File

@ -9,26 +9,18 @@ This overrides the dirstate to check with the eden daemon for modifications,
instead of doing a normal scan of the filesystem.
"""
from . import error, localrepo, merge as mergemod, progress, pycompat, util
from . import error, merge as mergemod, progress, pycompat, util
from .i18n import _
_repoclass = localrepo.localrepository
# This function is called by merge.update() in the fast path
# This function is called by merge.goto() in the fast path
# to ask the eden daemon to perform the update operation.
@util.timefunction("edenupdate", 0, "ui")
def update(
repo,
node,
branchmerge,
force,
ancestor=None,
mergeancestor=False,
force=False,
labels=None,
updatecheck=None,
wc=None,
):
repo.ui.debug("using eden update code path\n")
@ -138,7 +130,7 @@ def update(
repo.dirstate.clear()
# TODO(mbolin): Set the second parent, if appropriate.
repo.setparents(destctx.node())
mergemod.recordupdates(repo, actions, branchmerge)
mergemod.recordupdates(repo, actions, False)
# Clear the update state
util.unlink(vfs.join("updatestate"))

View File

@ -377,7 +377,7 @@ class state_update:
def enter(self):
# Make sure we have a wlock prior to sending notifications to watchman.
# We don't want to race with other actors. In the update case,
# merge.update is going to take the wlock almost immediately. We are
# merge.merge/goto is going to take the wlock almost immediately. We are
# effectively extending the lock around several short sanity checks.
if self.oldnode is None:
self.oldnode = self.repo["."].node()

View File

@ -39,7 +39,8 @@ _incompatible_exts = ["largefiles", "eol"]
def extsetup(ui):
extensions.wrapfunction(merge, "update", wrapupdate)
extensions.wrapfunction(merge, "goto", wrapgoto)
extensions.wrapfunction(merge, "merge", wrapmerge)
extensions.wrapfunction(filemerge, "_xmerge", _xmerge)
@ -103,11 +104,10 @@ def reposetup(ui, repo):
# and state-leave commands. This allows clients to perform more intelligent
# settling during bulk file change scenarios
# https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
def wrapupdate(
def wrapmerge(
orig,
repo,
node,
branchmerge=False,
wc=None,
**kwargs,
):
@ -117,7 +117,6 @@ def wrapupdate(
return orig(
repo,
node,
branchmerge=branchmerge,
wc=wc,
**kwargs,
)
@ -132,17 +131,42 @@ def wrapupdate(
oldnode=oldnode,
newnode=newnode,
distance=distance,
metadata={"merge": branchmerge},
metadata={"merge": True},
):
return orig(
repo,
node,
branchmerge=branchmerge,
wc=wc,
**kwargs,
)
def wrapgoto(
orig,
repo,
node,
**kwargs,
):
distance = 0
oldnode = repo["."].node()
newnode = repo[node].node()
distance = watchmanclient.calcdistance(repo, oldnode, newnode)
with watchmanclient.state_update(
repo,
name="hg.update",
oldnode=oldnode,
newnode=newnode,
distance=distance,
metadata={"merge": False},
):
return orig(
repo,
node,
**kwargs,
)
def _xmerge(origfunc, repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
# _xmerge is called when an external merge tool is invoked.
with state_filemerge(repo, fcd.path()):

View File

@ -913,7 +913,7 @@ class base(histeditaction):
with self.repo.wlock(), self.repo.lock(), self.repo.transaction(
"histedit-base"
):
mergemod.update(self.repo, self.node, force=True)
mergemod.goto(self.repo, self.node, force=True)
return self.continueclean()
def continuedirty(self):

View File

@ -1709,7 +1709,7 @@ def rebasenode(repo, rev, p1, base, state, collapse, dest, wctx):
else:
if repo["."].rev() != p1:
repo.ui.debug(" update to %s\n" % (repo[p1]))
mergemod.update(repo, p1, force=True)
mergemod.goto(repo, p1, force=True)
else:
repo.ui.debug(" already in destination\n")
# This is, alas, necessary to invalidate workingctx's manifest cache,
@ -1727,10 +1727,9 @@ def rebasenode(repo, rev, p1, base, state, collapse, dest, wctx):
labels = ["dest", "source"]
# When collapsing in-place, the parent is the common ancestor, we
# have to allow merging with it.
stats = mergemod.update(
stats = mergemod.merge(
repo,
rev,
branchmerge=True,
force=True,
ancestor=base,
mergeancestor=collapse,
@ -2159,7 +2158,7 @@ def abort(repo, originalwd, destmap, state, activebookmark=None) -> int:
# Update away from the rebase if necessary
if shouldupdate or needupdate(repo, state):
mergemod.update(repo, originalwd, force=True)
mergemod.goto(repo, originalwd, force=True)
# Strip from the first rebased revision
if rebased:

View File

@ -95,7 +95,7 @@ def _moveto(repo, bookmark, ctx, clean=False):
"""
# Move working copy over
if clean:
merge.update(
merge.goto(
repo,
ctx.node(),
force=True,

View File

@ -368,9 +368,9 @@ def _setupupdates(_ui) -> None:
extensions.wrapfunction(mergemod, "calculateupdates", _calculateupdates)
def _update(orig, repo, node, branchmerge=False, **kwargs):
def _goto(orig, repo, node, **kwargs):
try:
results = orig(repo, node, branchmerge=branchmerge, **kwargs)
results = orig(repo, node, **kwargs)
except Exception:
if _hassparse(repo):
repo._clearpendingprofileconfig()
@ -378,12 +378,12 @@ def _setupupdates(_ui) -> None:
# If we're updating to a location, clean up any stale temporary includes
# (ex: this happens during hg rebase --abort).
if not branchmerge and hasattr(repo, "sparsematch"):
if hasattr(repo, "sparsematch"):
repo.prunetemporaryincludes()
return results
extensions.wrapfunction(mergemod, "update", _update)
extensions.wrapfunction(mergemod, "goto", _goto)
def _setupcommit(ui) -> None:

View File

@ -166,9 +166,12 @@ def _runcommandwrapper(orig, lui, repo, cmd, fullargs, *args):
#
# To detect a write command, wrap all possible entries:
# - transaction.__init__
# - merge.update
# - merge.goto
# - merge.merge
w = extensions.wrappedfunction
with w(merge, "update", log), w(transaction.transaction, "__init__", log):
with w(merge, "goto", log), w(merge, "merge", log), w(
transaction.transaction, "__init__", log
):
try:
result = orig(lui, repo, cmd, fullargs, *args)
finally:

View File

@ -891,7 +891,7 @@ def updaterepo(repo, node, overwrite, updatecheck=None):
When overwrite is set, changes are clobbered, merged else
returns stats (see pydoc merge.applyupdates)"""
return mergemod.update(
return mergemod.goto(
repo,
node,
force=overwrite,
@ -1009,7 +1009,7 @@ def updatetotally(
def merge(repo, node, force=False, remind: bool = True, labels=None):
"""Branch merge with node, resolving changes. Return true if any
unresolved conflicts."""
stats = mergemod.update(repo, node, branchmerge=True, force=force, labels=labels)
stats = mergemod.merge(repo, node, force=force, labels=labels)
_showstats(repo, stats)
if stats[3]:
repo.ui.status(

View File

@ -767,7 +767,7 @@ def _checkunknownfiles(repo, wctx, mctx, force, actions):
# (1) this is probably the wrong behavior here -- we should
# probably abort, but some actions like rebases currently
# don't like an abort happening in the middle of
# merge.update.
# merge.update/goto.
if not different:
actions[f] = ("g", (fl2, False), "remote created")
elif config == "abort":
@ -1989,15 +1989,12 @@ def recordupdates(repo, actions, branchmerge):
prog.value += 1
def _logupdatedistance(ui, repo, node, branchmerge):
def _logupdatedistance(ui, repo, node):
"""Logs the update distance, if configured"""
# internal config: merge.recordupdatedistance
if not ui.configbool("merge", "recordupdatedistance", default=True):
return
if branchmerge:
return
try:
# The passed in node might actually be a rev, and if it's -1, that
# doesn't play nicely with revsets later because it resolve to the tip
@ -2074,9 +2071,122 @@ def _prefetchlazychildren(repo, node):
)
def goto(
repo,
node,
force=False,
labels=None,
updatecheck=None,
):
_logupdatedistance(repo.ui, repo, node)
_prefetchlazychildren(repo, node)
if not force:
# TODO: remove the default once all callers that pass force=False pass
# a value for updatecheck. We may want to allow updatecheck='abort' to
# better suppport some of these callers.
if updatecheck is None:
updatecheck = "linear"
assert updatecheck in ("none", "linear", "noconflict")
if edenfs.requirement in repo.requirements:
from . import eden_update
return eden_update.update(
repo,
node,
force=force,
labels=labels,
updatecheck=updatecheck,
)
# If we're doing the initial checkout from null, let's use the new fancier
# nativecheckout, since it has more efficient fetch mechanics.
# git backend only supports nativecheckout at present.
isclonecheckout = repo["."].node() == nullid
if (
repo.ui.configbool("experimental", "nativecheckout")
or (repo.ui.configbool("clone", "nativecheckout") and isclonecheckout)
or git.isgitstore(repo)
):
wc = repo[None]
if (
not isclonecheckout
and (force or updatecheck != "noconflict")
and (wc.dirty(missing=True) or mergestate.read(repo).active())
):
fallbackcheckout = (
"Working copy is dirty and --clean specified - not supported yet"
)
elif not hasattr(repo.fileslog, "contentstore"):
fallbackcheckout = "Repo does not have remotefilelog"
else:
fallbackcheckout = None
if fallbackcheckout:
repo.ui.debug("Not using native checkout: %s\n" % fallbackcheckout)
else:
# If the user is attempting to checkout for the first time, let's assume
# they don't have any pending changes and let's do a force checkout.
# This makes it much faster, by skipping the entire "check for unknown
# files" and "check for conflicts" code paths, and makes it so they
# aren't blocked by pending files and have to purge+clone over and over.
if isclonecheckout:
force = True
p1 = wc.parents()[0]
p2 = repo[node]
with repo.wlock():
ret = donativecheckout(
repo,
p1,
p2,
force,
wc,
querywatchmanrecrawls(repo),
)
if git.isgitformat(repo):
git.submodulecheckout(p2, force=force)
return ret
return _update(
repo,
node,
force=force,
labels=labels,
updatecheck=updatecheck,
)
def merge(
repo,
node,
force=False,
ancestor=None,
mergeancestor=False,
labels=None,
wc=None,
):
_prefetchlazychildren(repo, node)
return _update(
repo,
node,
branchmerge=True,
ancestor=ancestor,
mergeancestor=mergeancestor,
force=force,
labels=labels,
wc=wc,
)
@perftrace.tracefunc("Update")
@util.timefunction("mergeupdate", 0, "ui")
def update(
def _update(
repo,
node,
branchmerge=False,
@ -2141,55 +2251,9 @@ def update(
# that's now in destutil.py.
assert node is not None
_prefetchlazychildren(repo, node)
_logupdatedistance(repo.ui, repo, node, branchmerge)
# Positive indication we aren't using eden fastpath for eden integration tests.
if edenfs.requirement in repo.requirements:
if branchmerge:
# TODO: We potentially should support handling this scenario ourself in
# the future. For now we simply haven't investigated what the correct
# semantics are in this case.
why_not_eden = 'branchmerge is "truthy:" %s.' % branchmerge
elif ancestor is not None:
# TODO: We potentially should support handling this scenario ourself in
# the future. For now we simply haven't investigated what the correct
# semantics are in this case.
why_not_eden = "ancestor is not None: %s." % ancestor
elif wc is not None and wc.isinmemory():
# In memory merges do not operate on the working directory,
# so we don't need to ask eden to change the working directory state
# at all, and can use the vanilla merge logic in this case.
why_not_eden = "merge is in-memory"
else:
# TODO: Figure out what's the other cases here.
why_not_eden = None
if why_not_eden:
repo.ui.debug(
"falling back to non-eden update code path: %s\n" % why_not_eden
)
else:
from . import eden_update
return eden_update.update(
repo,
node,
branchmerge,
force,
ancestor,
mergeancestor,
labels,
updatecheck,
wc,
)
if not branchmerge and not force:
# TODO: remove the default once all callers that pass branchmerge=False
# and force=False pass a value for updatecheck. We may want to allow
# updatecheck='abort' to better suppport some of these callers.
if updatecheck is None:
updatecheck = "linear"
assert updatecheck in ("none", "linear", "noconflict")
repo.ui.debug("falling back to non-eden update code path: merge\n")
with repo.wlock():
prerecrawls = querywatchmanrecrawls(repo)
@ -2208,60 +2272,6 @@ def update(
fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
# If we're doing the initial checkout from null, let's use the new fancier
# nativecheckout, since it has more efficient fetch mechanics.
# git backend only supports nativecheckout at present.
isclonecheckout = repo["."].node() == nullid
# If the user is attempting to checkout for the first time, let's assume
# they don't have any pending changes and let's do a force checkout.
# This makes it much faster, by skipping the entire "check for unknown
# files" and "check for conflicts" code paths, and makes it so they
# aren't blocked by pending files and have to purge+clone over and over.
if isclonecheckout:
force = True
if (
repo.ui.configbool("experimental", "nativecheckout")
or (repo.ui.configbool("clone", "nativecheckout") and isclonecheckout)
or git.isgitstore(repo)
):
if branchmerge:
fallbackcheckout = "branchmerge is not supported: %s" % branchmerge
elif ancestor is not None:
fallbackcheckout = "ancestor is not supported: %s" % ancestor
elif wc is not None and wc.isinmemory():
fallbackcheckout = "Native checkout does not work inmemory"
elif (
not isclonecheckout
and (force or updatecheck != "noconflict")
and (wc.dirty(missing=True) or mergestate.read(repo).active())
):
fallbackcheckout = (
"Working copy is dirty and --clean specified - not supported yet"
)
elif not hasattr(repo.fileslog, "contentstore"):
fallbackcheckout = "Repo does not have remotefilelog"
else:
fallbackcheckout = None
if fallbackcheckout:
repo.ui.debug("Not using native checkout: %s\n" % fallbackcheckout)
else:
ret = donativecheckout(
repo,
p1,
p2,
xp1,
xp2,
force,
wc,
prerecrawls,
)
if git.isgitformat(repo) and not wc.isinmemory():
git.submodulecheckout(p2, force=force)
return ret
if pas[0] is None:
if repo.ui.configlist("merge", "preferancestor") == ["*"]:
cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
@ -2528,13 +2538,16 @@ def makenativecheckoutplan(repo, p1, p2, updateprogresspath=None):
@util.timefunction("donativecheckout", 0, "ui")
def donativecheckout(repo, p1, p2, xp1, xp2, force, wc, prerecrawls):
def donativecheckout(repo, p1, p2, force, wc, prerecrawls):
repo.ui.debug("Using native checkout\n")
repo.ui.log(
"nativecheckout",
using_nativecheckout=True,
)
xp1 = str(p1)
xp2 = str(p2)
updateprogresspath = None
if repo.ui.configbool("checkout", "resumable"):
updateprogresspath = repo.localvfs.join("updateprogress")
@ -2657,10 +2670,9 @@ def graft(repo, ctx, pctx, labels, keepparent=False):
# which local deleted".
mergeancestor = repo.changelog.isancestor(repo["."].node(), ctx.node())
stats = update(
stats = merge(
repo,
ctx.node(),
branchmerge=True,
force=True,
ancestor=pctx.node(),
mergeancestor=mergeancestor,

View File

@ -55,7 +55,7 @@ Now try prefetchchunksize option, and expect that two getpackv2 calls were made
[2]
$ hg up tip --config remotefilelog.prefetchchunksize=1 --debug
resolving manifests
branchmerge: False, force: True
branchmerge: False, force: False
ancestor: 000000000000, local: 000000000000+, remote: 79c51fb96423
2 files updated, 0 files merged, 0 files removed, 0 files unresolved

View File

@ -334,7 +334,7 @@ Test for issue2364
$ hg up -q -- -2
Test that updated files are treated as "modified", when
'merge.update()' is aborted before 'merge.recordupdates()' (= parents
'merge.goto()' is aborted before 'merge.recordupdates()' (= parents
aren't changed), even if none of mode, size and timestamp of them
isn't changed on the filesystem (see also issue4583).