diff --git a/eden/scm/sapling/commands/__init__.py b/eden/scm/sapling/commands/__init__.py index ff27dc68b80d5..c8e223b2f912e 100644 --- a/eden/scm/sapling/commands/__init__.py +++ b/eden/scm/sapling/commands/__init__.py @@ -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) diff --git a/eden/scm/sapling/eden_update.py b/eden/scm/sapling/eden_update.py index 7b6052e825d5d..ed218d8a07c0b 100644 --- a/eden/scm/sapling/eden_update.py +++ b/eden/scm/sapling/eden_update.py @@ -9,26 +9,18 @@ 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")) diff --git a/eden/scm/sapling/ext/extlib/watchmanclient/__init__.py b/eden/scm/sapling/ext/extlib/watchmanclient/__init__.py index 06a6b3c99a1ab..b2b05df08c78d 100644 --- a/eden/scm/sapling/ext/extlib/watchmanclient/__init__.py +++ b/eden/scm/sapling/ext/extlib/watchmanclient/__init__.py @@ -377,7 +377,7 @@ def __enter__(self): 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() diff --git a/eden/scm/sapling/ext/hgevents/__init__.py b/eden/scm/sapling/ext/hgevents/__init__.py index 2c7d28ffd902c..91d5e9a663d7a 100644 --- a/eden/scm/sapling/ext/hgevents/__init__.py +++ b/eden/scm/sapling/ext/hgevents/__init__.py @@ -39,7 +39,8 @@ 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 staterelease(): # 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()): diff --git a/eden/scm/sapling/ext/histedit.py b/eden/scm/sapling/ext/histedit.py index 1b43d811acb8e..9dd9e928a2b1d 100644 --- a/eden/scm/sapling/ext/histedit.py +++ b/eden/scm/sapling/ext/histedit.py @@ -913,7 +913,7 @@ def run(self): 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): diff --git a/eden/scm/sapling/ext/rebase.py b/eden/scm/sapling/ext/rebase.py index dbc6ccb812e97..048548f5f70ff 100644 --- a/eden/scm/sapling/ext/rebase.py +++ b/eden/scm/sapling/ext/rebase.py @@ -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: diff --git a/eden/scm/sapling/ext/reset.py b/eden/scm/sapling/ext/reset.py index 21ae46b4a2fb1..20e27c2f7d3bf 100644 --- a/eden/scm/sapling/ext/reset.py +++ b/eden/scm/sapling/ext/reset.py @@ -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, diff --git a/eden/scm/sapling/ext/sparse.py b/eden/scm/sapling/ext/sparse.py index ec1d7f16889d8..3b0aafd602819 100644 --- a/eden/scm/sapling/ext/sparse.py +++ b/eden/scm/sapling/ext/sparse.py @@ -368,9 +368,9 @@ def _calculateupdates( 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 _update(orig, repo, node, branchmerge=False, **kwargs): # 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: diff --git a/eden/scm/sapling/ext/undo.py b/eden/scm/sapling/ext/undo.py index fc64ad6ed940f..37fc6fc36509c 100644 --- a/eden/scm/sapling/ext/undo.py +++ b/eden/scm/sapling/ext/undo.py @@ -166,9 +166,12 @@ def log(orig, *args, **kwargs): # # 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: diff --git a/eden/scm/sapling/hg.py b/eden/scm/sapling/hg.py index 593dbb9075e82..7bf47a9f56390 100644 --- a/eden/scm/sapling/hg.py +++ b/eden/scm/sapling/hg.py @@ -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( diff --git a/eden/scm/sapling/merge.py b/eden/scm/sapling/merge.py index 07ab3659522a3..4db6dc3525a00 100644 --- a/eden/scm/sapling/merge.py +++ b/eden/scm/sapling/merge.py @@ -767,7 +767,7 @@ def collectconflicts(conflicts, config): # (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, diff --git a/eden/scm/tests/test-fb-ext-remotefilelog-getpackv2.t b/eden/scm/tests/test-fb-ext-remotefilelog-getpackv2.t index 7c74731badc8c..5dd00e072af12 100644 --- a/eden/scm/tests/test-fb-ext-remotefilelog-getpackv2.t +++ b/eden/scm/tests/test-fb-ext-remotefilelog-getpackv2.t @@ -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 diff --git a/eden/scm/tests/test-merge1.t b/eden/scm/tests/test-merge1.t index c65d2b270d26f..b94986accd1ac 100644 --- a/eden/scm/tests/test-merge1.t +++ b/eden/scm/tests/test-merge1.t @@ -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).