mirror of
https://github.com/facebook/sapling.git
synced 2024-10-05 14:28:17 +03:00
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:
parent
5144e52f36
commit
b22ca6b910
@ -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)
|
||||
|
||||
|
@ -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"))
|
||||
|
@ -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()
|
||||
|
@ -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()):
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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).
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user