[Request] [+------] D8550: mergestate: split out merge state handling code from main merge module

durin42 (Augie Fackler) phabricator at mercurial-scm.org
Mon May 18 22:13:53 UTC 2020


durin42 created this revision.
Herald added a reviewer: martinvonz.
Herald added a reviewer: hg-reviewers.
Herald added a subscriber: mercurial-patches.

REVISION SUMMARY
  There's already some pretty reasonable encapsulation here, but I want
  to make the mergestate storage a property of the context so memctx
  instances can do a reasonable thing. This is the first step in a
  reshuffle to make that easier.

REPOSITORY
  rHG Mercurial

BRANCH
  default

REVISION DETAIL
  https://phab.mercurial-scm.org/D8550

AFFECTED FILES
  hgext/fix.py
  hgext/histedit.py
  hgext/largefiles/overrides.py
  hgext/rebase.py
  hgext/strip.py
  mercurial/cmdutil.py
  mercurial/commands.py
  mercurial/debugcommands.py
  mercurial/fileset.py
  mercurial/hg.py
  mercurial/localrepo.py
  mercurial/merge.py
  mercurial/mergestate.py
  mercurial/narrowspec.py
  mercurial/revset.py
  mercurial/shelve.py
  mercurial/sparse.py
  mercurial/templatekw.py
  tests/fakemergerecord.py
  tests/test-dirstate.t
  tests/test-resolve.t

CHANGE DETAILS

diff --git a/tests/test-resolve.t b/tests/test-resolve.t
--- a/tests/test-resolve.t
+++ b/tests/test-resolve.t
@@ -92,7 +92,7 @@
   $ cat > $TESTTMP/markdriver.py << EOF
   > '''mark and unmark files as driver-resolved'''
   > from mercurial import (
-  >    merge,
+  >    mergestate,
   >    pycompat,
   >    registrar,
   >    scmutil,
@@ -106,7 +106,7 @@
   >     wlock = repo.wlock()
   >     opts = pycompat.byteskwargs(opts)
   >     try:
-  >         ms = merge.mergestate.read(repo)
+  >         ms = mergestate.mergestate.read(repo)
   >         m = scmutil.match(repo[None], pats, opts)
   >         for f in ms:
   >             if not m(f):
diff --git a/tests/test-dirstate.t b/tests/test-dirstate.t
--- a/tests/test-dirstate.t
+++ b/tests/test-dirstate.t
@@ -70,14 +70,15 @@
   > from mercurial import (
   >   error,
   >   extensions,
-  >   merge,
+  >   mergestate as mergestatemod,
   > )
   > 
   > def wraprecordupdates(*args):
   >     raise error.Abort("simulated error while recording dirstateupdates")
   > 
   > def reposetup(ui, repo):
-  >     extensions.wrapfunction(merge, 'recordupdates', wraprecordupdates)
+  >     extensions.wrapfunction(mergestatemod, 'recordupdates',
+  >                             wraprecordupdates)
   > EOF
 
   $ hg rm a
diff --git a/tests/fakemergerecord.py b/tests/fakemergerecord.py
--- a/tests/fakemergerecord.py
+++ b/tests/fakemergerecord.py
@@ -5,7 +5,7 @@
 from __future__ import absolute_import
 
 from mercurial import (
-    merge,
+    mergestate as mergestatemod,
     registrar,
 )
 
@@ -23,7 +23,7 @@
 )
 def fakemergerecord(ui, repo, *pats, **opts):
     with repo.wlock():
-        ms = merge.mergestate.read(repo)
+        ms = mergestatemod.mergestate.read(repo)
         records = ms._makerecords()
         if opts.get('mandatory'):
             records.append((b'X', b'mandatory record'))
diff --git a/mercurial/templatekw.py b/mercurial/templatekw.py
--- a/mercurial/templatekw.py
+++ b/mercurial/templatekw.py
@@ -419,9 +419,9 @@
     else:
         merge_nodes = cache.get(b'merge_nodes')
         if merge_nodes is None:
-            from . import merge
+            from . import mergestate as mergestatemod
 
-            mergestate = merge.mergestate.read(repo)
+            mergestate = mergestatemod.mergestate.read(repo)
             if mergestate.active():
                 merge_nodes = (mergestate.local, mergestate.other)
             else:
diff --git a/mercurial/sparse.py b/mercurial/sparse.py
--- a/mercurial/sparse.py
+++ b/mercurial/sparse.py
@@ -18,6 +18,7 @@
     error,
     match as matchmod,
     merge as mergemod,
+    mergestate as mergestatemod,
     pathutil,
     pycompat,
     scmutil,
@@ -406,7 +407,7 @@
         elif file in wctx:
             prunedactions[file] = (b'r', args, msg)
 
-        if branchmerge and type == mergemod.ACTION_MERGE:
+        if branchmerge and type == mergestatemod.ACTION_MERGE:
             f1, f2, fa, move, anc = args
             if not sparsematch(f1):
                 temporaryfiles.append(f1)
diff --git a/mercurial/shelve.py b/mercurial/shelve.py
--- a/mercurial/shelve.py
+++ b/mercurial/shelve.py
@@ -42,6 +42,7 @@
     lock as lockmod,
     mdiff,
     merge,
+    mergestate as mergestatemod,
     node as nodemod,
     patch,
     phases,
@@ -801,7 +802,7 @@
     basename = state.name
     with repo.lock():
         checkparents(repo, state)
-        ms = merge.mergestate.read(repo)
+        ms = mergestatemod.mergestate.read(repo)
         if list(ms.unresolved()):
             raise error.Abort(
                 _(b"unresolved conflicts, can't continue"),
diff --git a/mercurial/revset.py b/mercurial/revset.py
--- a/mercurial/revset.py
+++ b/mercurial/revset.py
@@ -789,9 +789,9 @@
     "merge" here includes merge conflicts from e.g. 'hg rebase' or 'hg graft'.
     """
     getargs(x, 0, 0, _(b"conflictlocal takes no arguments"))
-    from . import merge
-
-    mergestate = merge.mergestate.read(repo)
+    from . import mergestate as mergestatemod
+
+    mergestate = mergestatemod.mergestate.read(repo)
     if mergestate.active() and repo.changelog.hasnode(mergestate.local):
         return subset & {repo.changelog.rev(mergestate.local)}
 
@@ -805,9 +805,9 @@
     "merge" here includes merge conflicts from e.g. 'hg rebase' or 'hg graft'.
     """
     getargs(x, 0, 0, _(b"conflictother takes no arguments"))
-    from . import merge
-
-    mergestate = merge.mergestate.read(repo)
+    from . import mergestate as mergestatemod
+
+    mergestate = mergestatemod.mergestate.read(repo)
     if mergestate.active() and repo.changelog.hasnode(mergestate.other):
         return subset & {repo.changelog.rev(mergestate.other)}
 
diff --git a/mercurial/narrowspec.py b/mercurial/narrowspec.py
--- a/mercurial/narrowspec.py
+++ b/mercurial/narrowspec.py
@@ -14,6 +14,7 @@
     error,
     match as matchmod,
     merge,
+    mergestate as mergestatemod,
     scmutil,
     sparse,
     util,
@@ -272,7 +273,7 @@
 
 def _writeaddedfiles(repo, pctx, files):
     actions = merge.emptyactions()
-    addgaction = actions[merge.ACTION_GET].append
+    addgaction = actions[mergestatemod.ACTION_GET].append
     mf = repo[b'.'].manifest()
     for f in files:
         if not repo.wvfs.exists(f):
diff --git a/mercurial/merge.py b/mercurial/mergestate.py
copy from mercurial/merge.py
copy to mercurial/mergestate.py
--- a/mercurial/merge.py
+++ b/mercurial/mergestate.py
@@ -1,42 +1,22 @@
-# merge.py - directory-level update/merge handling for Mercurial
-#
-# Copyright 2006, 2007 Matt Mackall <mpm at selenic.com>
-#
-# 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
 
 import errno
 import shutil
-import stat
 import struct
 
 from .i18n import _
 from .node import (
-    addednodeid,
     bin,
     hex,
-    modifiednodeid,
     nullhex,
     nullid,
-    nullrev,
 )
 from .pycompat import delattr
-from .thirdparty import attr
 from . import (
-    copies,
-    encoding,
     error,
     filemerge,
-    match as matchmod,
-    obsutil,
-    pathutil,
     pycompat,
-    scmutil,
-    subrepoutil,
     util,
-    worker,
 )
 from .utils import hashutil
 
@@ -773,1370 +753,6 @@
         self._results[f] = 0, ACTION_GET
 
 
-def _getcheckunknownconfig(repo, section, name):
-    config = repo.ui.config(section, name)
-    valid = [b'abort', b'ignore', b'warn']
-    if config not in valid:
-        validstr = b', '.join([b"'" + v + b"'" for v in valid])
-        raise error.ConfigError(
-            _(b"%s.%s not valid ('%s' is none of %s)")
-            % (section, name, config, validstr)
-        )
-    return config
-
-
-def _checkunknownfile(repo, wctx, mctx, f, f2=None):
-    if wctx.isinmemory():
-        # Nothing to do in IMM because nothing in the "working copy" can be an
-        # unknown file.
-        #
-        # Note that we should bail out here, not in ``_checkunknownfiles()``,
-        # because that function does other useful work.
-        return False
-
-    if f2 is None:
-        f2 = f
-    return (
-        repo.wvfs.audit.check(f)
-        and repo.wvfs.isfileorlink(f)
-        and repo.dirstate.normalize(f) not in repo.dirstate
-        and mctx[f2].cmp(wctx[f])
-    )
-
-
-class _unknowndirschecker(object):
-    """
-    Look for any unknown files or directories that may have a path conflict
-    with a file.  If any path prefix of the file exists as a file or link,
-    then it conflicts.  If the file itself is a directory that contains any
-    file that is not tracked, then it conflicts.
-
-    Returns the shortest path at which a conflict occurs, or None if there is
-    no conflict.
-    """
-
-    def __init__(self):
-        # A set of paths known to be good.  This prevents repeated checking of
-        # dirs.  It will be updated with any new dirs that are checked and found
-        # to be safe.
-        self._unknowndircache = set()
-
-        # A set of paths that are known to be absent.  This prevents repeated
-        # checking of subdirectories that are known not to exist. It will be
-        # updated with any new dirs that are checked and found to be absent.
-        self._missingdircache = set()
-
-    def __call__(self, repo, wctx, f):
-        if wctx.isinmemory():
-            # Nothing to do in IMM for the same reason as ``_checkunknownfile``.
-            return False
-
-        # Check for path prefixes that exist as unknown files.
-        for p in reversed(list(pathutil.finddirs(f))):
-            if p in self._missingdircache:
-                return
-            if p in self._unknowndircache:
-                continue
-            if repo.wvfs.audit.check(p):
-                if (
-                    repo.wvfs.isfileorlink(p)
-                    and repo.dirstate.normalize(p) not in repo.dirstate
-                ):
-                    return p
-                if not repo.wvfs.lexists(p):
-                    self._missingdircache.add(p)
-                    return
-                self._unknowndircache.add(p)
-
-        # Check if the file conflicts with a directory containing unknown files.
-        if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
-            # Does the directory contain any files that are not in the dirstate?
-            for p, dirs, files in repo.wvfs.walk(f):
-                for fn in files:
-                    relf = util.pconvert(repo.wvfs.reljoin(p, fn))
-                    relf = repo.dirstate.normalize(relf, isknown=True)
-                    if relf not in repo.dirstate:
-                        return f
-        return None
-
-
-def _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce):
-    """
-    Considers any actions that care about the presence of conflicting unknown
-    files. For some actions, the result is to abort; for others, it is to
-    choose a different action.
-    """
-    fileconflicts = set()
-    pathconflicts = set()
-    warnconflicts = set()
-    abortconflicts = set()
-    unknownconfig = _getcheckunknownconfig(repo, b'merge', b'checkunknown')
-    ignoredconfig = _getcheckunknownconfig(repo, b'merge', b'checkignored')
-    pathconfig = repo.ui.configbool(
-        b'experimental', b'merge.checkpathconflicts'
-    )
-    if not force:
-
-        def collectconflicts(conflicts, config):
-            if config == b'abort':
-                abortconflicts.update(conflicts)
-            elif config == b'warn':
-                warnconflicts.update(conflicts)
-
-        checkunknowndirs = _unknowndirschecker()
-        for f, (m, args, msg) in pycompat.iteritems(actions):
-            if m in (ACTION_CREATED, ACTION_DELETED_CHANGED):
-                if _checkunknownfile(repo, wctx, mctx, f):
-                    fileconflicts.add(f)
-                elif pathconfig and f not in wctx:
-                    path = checkunknowndirs(repo, wctx, f)
-                    if path is not None:
-                        pathconflicts.add(path)
-            elif m == ACTION_LOCAL_DIR_RENAME_GET:
-                if _checkunknownfile(repo, wctx, mctx, f, args[0]):
-                    fileconflicts.add(f)
-
-        allconflicts = fileconflicts | pathconflicts
-        ignoredconflicts = {c for c in allconflicts if repo.dirstate._ignore(c)}
-        unknownconflicts = allconflicts - ignoredconflicts
-        collectconflicts(ignoredconflicts, ignoredconfig)
-        collectconflicts(unknownconflicts, unknownconfig)
-    else:
-        for f, (m, args, msg) in pycompat.iteritems(actions):
-            if m == ACTION_CREATED_MERGE:
-                fl2, anc = args
-                different = _checkunknownfile(repo, wctx, mctx, f)
-                if repo.dirstate._ignore(f):
-                    config = ignoredconfig
-                else:
-                    config = unknownconfig
-
-                # The behavior when force is True is described by this table:
-                #  config  different  mergeforce  |    action    backup
-                #    *         n          *       |      get        n
-                #    *         y          y       |     merge       -
-                #   abort      y          n       |     merge       -   (1)
-                #   warn       y          n       |  warn + get     y
-                #  ignore      y          n       |      get        y
-                #
-                # (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.
-                if not different:
-                    actions[f] = (ACTION_GET, (fl2, False), b'remote created')
-                elif mergeforce or config == b'abort':
-                    actions[f] = (
-                        ACTION_MERGE,
-                        (f, f, None, False, anc),
-                        b'remote differs from untracked local',
-                    )
-                elif config == b'abort':
-                    abortconflicts.add(f)
-                else:
-                    if config == b'warn':
-                        warnconflicts.add(f)
-                    actions[f] = (ACTION_GET, (fl2, True), b'remote created')
-
-    for f in sorted(abortconflicts):
-        warn = repo.ui.warn
-        if f in pathconflicts:
-            if repo.wvfs.isfileorlink(f):
-                warn(_(b"%s: untracked file conflicts with directory\n") % f)
-            else:
-                warn(_(b"%s: untracked directory conflicts with file\n") % f)
-        else:
-            warn(_(b"%s: untracked file differs\n") % f)
-    if abortconflicts:
-        raise error.Abort(
-            _(
-                b"untracked files in working directory "
-                b"differ from files in requested revision"
-            )
-        )
-
-    for f in sorted(warnconflicts):
-        if repo.wvfs.isfileorlink(f):
-            repo.ui.warn(_(b"%s: replacing untracked file\n") % f)
-        else:
-            repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f)
-
-    for f, (m, args, msg) in pycompat.iteritems(actions):
-        if m == ACTION_CREATED:
-            backup = (
-                f in fileconflicts
-                or f in pathconflicts
-                or any(p in pathconflicts for p in pathutil.finddirs(f))
-            )
-            (flags,) = args
-            actions[f] = (ACTION_GET, (flags, backup), msg)
-
-
-def _forgetremoved(wctx, mctx, branchmerge):
-    """
-    Forget removed files
-
-    If we're jumping between revisions (as opposed to merging), and if
-    neither the working directory nor the target rev has the file,
-    then we need to remove it from the dirstate, to prevent the
-    dirstate from listing the file when it is no longer in the
-    manifest.
-
-    If we're merging, and the other revision has removed a file
-    that is not present in the working directory, we need to mark it
-    as removed.
-    """
-
-    actions = {}
-    m = ACTION_FORGET
-    if branchmerge:
-        m = ACTION_REMOVE
-    for f in wctx.deleted():
-        if f not in mctx:
-            actions[f] = m, None, b"forget deleted"
-
-    if not branchmerge:
-        for f in wctx.removed():
-            if f not in mctx:
-                actions[f] = ACTION_FORGET, None, b"forget removed"
-
-    return actions
-
-
-def _checkcollision(repo, wmf, actions):
-    """
-    Check for case-folding collisions.
-    """
-    # If the repo is narrowed, filter out files outside the narrowspec.
-    narrowmatch = repo.narrowmatch()
-    if not narrowmatch.always():
-        pmmf = set(wmf.walk(narrowmatch))
-        if actions:
-            narrowactions = {}
-            for m, actionsfortype in pycompat.iteritems(actions):
-                narrowactions[m] = []
-                for (f, args, msg) in actionsfortype:
-                    if narrowmatch(f):
-                        narrowactions[m].append((f, args, msg))
-            actions = narrowactions
-    else:
-        # build provisional merged manifest up
-        pmmf = set(wmf)
-
-    if actions:
-        # KEEP and EXEC are no-op
-        for m in (
-            ACTION_ADD,
-            ACTION_ADD_MODIFIED,
-            ACTION_FORGET,
-            ACTION_GET,
-            ACTION_CHANGED_DELETED,
-            ACTION_DELETED_CHANGED,
-        ):
-            for f, args, msg in actions[m]:
-                pmmf.add(f)
-        for f, args, msg in actions[ACTION_REMOVE]:
-            pmmf.discard(f)
-        for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
-            f2, flags = args
-            pmmf.discard(f2)
-            pmmf.add(f)
-        for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
-            pmmf.add(f)
-        for f, args, msg in actions[ACTION_MERGE]:
-            f1, f2, fa, move, anc = args
-            if move:
-                pmmf.discard(f1)
-            pmmf.add(f)
-
-    # check case-folding collision in provisional merged manifest
-    foldmap = {}
-    for f in pmmf:
-        fold = util.normcase(f)
-        if fold in foldmap:
-            raise error.Abort(
-                _(b"case-folding collision between %s and %s")
-                % (f, foldmap[fold])
-            )
-        foldmap[fold] = f
-
-    # check case-folding of directories
-    foldprefix = unfoldprefix = lastfull = b''
-    for fold, f in sorted(foldmap.items()):
-        if fold.startswith(foldprefix) and not f.startswith(unfoldprefix):
-            # the folded prefix matches but actual casing is different
-            raise error.Abort(
-                _(b"case-folding collision between %s and directory of %s")
-                % (lastfull, f)
-            )
-        foldprefix = fold + b'/'
-        unfoldprefix = f + b'/'
-        lastfull = f
-
-
-def driverpreprocess(repo, ms, wctx, labels=None):
-    """run the preprocess step of the merge driver, if any
-
-    This is currently not implemented -- it's an extension point."""
-    return True
-
-
-def driverconclude(repo, ms, wctx, labels=None):
-    """run the conclude step of the merge driver, if any
-
-    This is currently not implemented -- it's an extension point."""
-    return True
-
-
-def _filesindirs(repo, manifest, dirs):
-    """
-    Generator that yields pairs of all the files in the manifest that are found
-    inside the directories listed in dirs, and which directory they are found
-    in.
-    """
-    for f in manifest:
-        for p in pathutil.finddirs(f):
-            if p in dirs:
-                yield f, p
-                break
-
-
-def checkpathconflicts(repo, wctx, mctx, actions):
-    """
-    Check if any actions introduce path conflicts in the repository, updating
-    actions to record or handle the path conflict accordingly.
-    """
-    mf = wctx.manifest()
-
-    # The set of local files that conflict with a remote directory.
-    localconflicts = set()
-
-    # The set of directories that conflict with a remote file, and so may cause
-    # conflicts if they still contain any files after the merge.
-    remoteconflicts = set()
-
-    # The set of directories that appear as both a file and a directory in the
-    # remote manifest.  These indicate an invalid remote manifest, which
-    # can't be updated to cleanly.
-    invalidconflicts = set()
-
-    # The set of directories that contain files that are being created.
-    createdfiledirs = set()
-
-    # The set of files deleted by all the actions.
-    deletedfiles = set()
-
-    for f, (m, args, msg) in actions.items():
-        if m in (
-            ACTION_CREATED,
-            ACTION_DELETED_CHANGED,
-            ACTION_MERGE,
-            ACTION_CREATED_MERGE,
-        ):
-            # This action may create a new local file.
-            createdfiledirs.update(pathutil.finddirs(f))
-            if mf.hasdir(f):
-                # The file aliases a local directory.  This might be ok if all
-                # the files in the local directory are being deleted.  This
-                # will be checked once we know what all the deleted files are.
-                remoteconflicts.add(f)
-        # Track the names of all deleted files.
-        if m == ACTION_REMOVE:
-            deletedfiles.add(f)
-        if m == ACTION_MERGE:
-            f1, f2, fa, move, anc = args
-            if move:
-                deletedfiles.add(f1)
-        if m == ACTION_DIR_RENAME_MOVE_LOCAL:
-            f2, flags = args
-            deletedfiles.add(f2)
-
-    # Check all directories that contain created files for path conflicts.
-    for p in createdfiledirs:
-        if p in mf:
-            if p in mctx:
-                # A file is in a directory which aliases both a local
-                # and a remote file.  This is an internal inconsistency
-                # within the remote manifest.
-                invalidconflicts.add(p)
-            else:
-                # A file is in a directory which aliases a local file.
-                # We will need to rename the local file.
-                localconflicts.add(p)
-        if p in actions and actions[p][0] in (
-            ACTION_CREATED,
-            ACTION_DELETED_CHANGED,
-            ACTION_MERGE,
-            ACTION_CREATED_MERGE,
-        ):
-            # The file is in a directory which aliases a remote file.
-            # This is an internal inconsistency within the remote
-            # manifest.
-            invalidconflicts.add(p)
-
-    # Rename all local conflicting files that have not been deleted.
-    for p in localconflicts:
-        if p not in deletedfiles:
-            ctxname = bytes(wctx).rstrip(b'+')
-            pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
-            actions[pnew] = (
-                ACTION_PATH_CONFLICT_RESOLVE,
-                (p,),
-                b'local path conflict',
-            )
-            actions[p] = (ACTION_PATH_CONFLICT, (pnew, b'l'), b'path conflict')
-
-    if remoteconflicts:
-        # Check if all files in the conflicting directories have been removed.
-        ctxname = bytes(mctx).rstrip(b'+')
-        for f, p in _filesindirs(repo, mf, remoteconflicts):
-            if f not in deletedfiles:
-                m, args, msg = actions[p]
-                pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
-                if m in (ACTION_DELETED_CHANGED, ACTION_MERGE):
-                    # Action was merge, just update target.
-                    actions[pnew] = (m, args, msg)
-                else:
-                    # Action was create, change to renamed get action.
-                    fl = args[0]
-                    actions[pnew] = (
-                        ACTION_LOCAL_DIR_RENAME_GET,
-                        (p, fl),
-                        b'remote path conflict',
-                    )
-                actions[p] = (
-                    ACTION_PATH_CONFLICT,
-                    (pnew, ACTION_REMOVE),
-                    b'path conflict',
-                )
-                remoteconflicts.remove(p)
-                break
-
-    if invalidconflicts:
-        for p in invalidconflicts:
-            repo.ui.warn(_(b"%s: is both a file and a directory\n") % p)
-        raise error.Abort(_(b"destination manifest contains path conflicts"))
-
-
-def _filternarrowactions(narrowmatch, branchmerge, actions):
-    """
-    Filters out actions that can ignored because the repo is narrowed.
-
-    Raise an exception if the merge cannot be completed because the repo is
-    narrowed.
-    """
-    nooptypes = {b'k'}  # TODO: handle with nonconflicttypes
-    nonconflicttypes = set(b'a am c cm f g gs r e'.split())
-    # We mutate the items in the dict during iteration, so iterate
-    # over a copy.
-    for f, action in list(actions.items()):
-        if narrowmatch(f):
-            pass
-        elif not branchmerge:
-            del actions[f]  # just updating, ignore changes outside clone
-        elif action[0] in nooptypes:
-            del actions[f]  # merge does not affect file
-        elif action[0] in nonconflicttypes:
-            raise error.Abort(
-                _(
-                    b'merge affects file \'%s\' outside narrow, '
-                    b'which is not yet supported'
-                )
-                % f,
-                hint=_(b'merging in the other direction may work'),
-            )
-        else:
-            raise error.Abort(
-                _(b'conflict in file \'%s\' is outside narrow clone') % f
-            )
-
-
-def manifestmerge(
-    repo,
-    wctx,
-    p2,
-    pa,
-    branchmerge,
-    force,
-    matcher,
-    acceptremote,
-    followcopies,
-    forcefulldiff=False,
-):
-    """
-    Merge wctx and p2 with ancestor pa and generate merge action list
-
-    branchmerge and force are as passed in to update
-    matcher = matcher to filter file lists
-    acceptremote = accept the incoming changes without prompting
-    """
-    if matcher is not None and matcher.always():
-        matcher = None
-
-    # manifests fetched in order are going to be faster, so prime the caches
-    [
-        x.manifest()
-        for x in sorted(wctx.parents() + [p2, pa], key=scmutil.intrev)
-    ]
-
-    branch_copies1 = copies.branch_copies()
-    branch_copies2 = copies.branch_copies()
-    diverge = {}
-    if followcopies:
-        branch_copies1, branch_copies2, diverge = copies.mergecopies(
-            repo, wctx, p2, pa
-        )
-
-    boolbm = pycompat.bytestr(bool(branchmerge))
-    boolf = pycompat.bytestr(bool(force))
-    boolm = pycompat.bytestr(bool(matcher))
-    repo.ui.note(_(b"resolving manifests\n"))
-    repo.ui.debug(
-        b" branchmerge: %s, force: %s, partial: %s\n" % (boolbm, boolf, boolm)
-    )
-    repo.ui.debug(b" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
-
-    m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
-    copied1 = set(branch_copies1.copy.values())
-    copied1.update(branch_copies1.movewithdir.values())
-    copied2 = set(branch_copies2.copy.values())
-    copied2.update(branch_copies2.movewithdir.values())
-
-    if b'.hgsubstate' in m1 and wctx.rev() is None:
-        # Check whether sub state is modified, and overwrite the manifest
-        # to flag the change. If wctx is a committed revision, we shouldn't
-        # care for the dirty state of the working directory.
-        if any(wctx.sub(s).dirty() for s in wctx.substate):
-            m1[b'.hgsubstate'] = modifiednodeid
-
-    # Don't use m2-vs-ma optimization if:
-    # - ma is the same as m1 or m2, which we're just going to diff again later
-    # - The caller specifically asks for a full diff, which is useful during bid
-    #   merge.
-    if pa not in ([wctx, p2] + wctx.parents()) and not forcefulldiff:
-        # Identify which files are relevant to the merge, so we can limit the
-        # total m1-vs-m2 diff to just those files. This has significant
-        # performance benefits in large repositories.
-        relevantfiles = set(ma.diff(m2).keys())
-
-        # For copied and moved files, we need to add the source file too.
-        for copykey, copyvalue in pycompat.iteritems(branch_copies1.copy):
-            if copyvalue in relevantfiles:
-                relevantfiles.add(copykey)
-        for movedirkey in branch_copies1.movewithdir:
-            relevantfiles.add(movedirkey)
-        filesmatcher = scmutil.matchfiles(repo, relevantfiles)
-        matcher = matchmod.intersectmatchers(matcher, filesmatcher)
-
-    diff = m1.diff(m2, match=matcher)
-
-    actions = {}
-    for f, ((n1, fl1), (n2, fl2)) in pycompat.iteritems(diff):
-        if n1 and n2:  # file exists on both local and remote side
-            if f not in ma:
-                # TODO: what if they're renamed from different sources?
-                fa = branch_copies1.copy.get(
-                    f, None
-                ) or branch_copies2.copy.get(f, None)
-                if fa is not None:
-                    actions[f] = (
-                        ACTION_MERGE,
-                        (f, f, fa, False, pa.node()),
-                        b'both renamed from %s' % fa,
-                    )
-                else:
-                    actions[f] = (
-                        ACTION_MERGE,
-                        (f, f, None, False, pa.node()),
-                        b'both created',
-                    )
-            else:
-                a = ma[f]
-                fla = ma.flags(f)
-                nol = b'l' not in fl1 + fl2 + fla
-                if n2 == a and fl2 == fla:
-                    actions[f] = (ACTION_KEEP, (), b'remote unchanged')
-                elif n1 == a and fl1 == fla:  # local unchanged - use remote
-                    if n1 == n2:  # optimization: keep local content
-                        actions[f] = (
-                            ACTION_EXEC,
-                            (fl2,),
-                            b'update permissions',
-                        )
-                    else:
-                        actions[f] = (
-                            ACTION_GET_OTHER_AND_STORE
-                            if branchmerge
-                            else ACTION_GET,
-                            (fl2, False),
-                            b'remote is newer',
-                        )
-                elif nol and n2 == a:  # remote only changed 'x'
-                    actions[f] = (ACTION_EXEC, (fl2,), b'update permissions')
-                elif nol and n1 == a:  # local only changed 'x'
-                    actions[f] = (
-                        ACTION_GET_OTHER_AND_STORE
-                        if branchmerge
-                        else ACTION_GET,
-                        (fl1, False),
-                        b'remote is newer',
-                    )
-                else:  # both changed something
-                    actions[f] = (
-                        ACTION_MERGE,
-                        (f, f, f, False, pa.node()),
-                        b'versions differ',
-                    )
-        elif n1:  # file exists only on local side
-            if f in copied2:
-                pass  # we'll deal with it on m2 side
-            elif (
-                f in branch_copies1.movewithdir
-            ):  # directory rename, move local
-                f2 = branch_copies1.movewithdir[f]
-                if f2 in m2:
-                    actions[f2] = (
-                        ACTION_MERGE,
-                        (f, f2, None, True, pa.node()),
-                        b'remote directory rename, both created',
-                    )
-                else:
-                    actions[f2] = (
-                        ACTION_DIR_RENAME_MOVE_LOCAL,
-                        (f, fl1),
-                        b'remote directory rename - move from %s' % f,
-                    )
-            elif f in branch_copies1.copy:
-                f2 = branch_copies1.copy[f]
-                actions[f] = (
-                    ACTION_MERGE,
-                    (f, f2, f2, False, pa.node()),
-                    b'local copied/moved from %s' % f2,
-                )
-            elif f in ma:  # clean, a different, no remote
-                if n1 != ma[f]:
-                    if acceptremote:
-                        actions[f] = (ACTION_REMOVE, None, b'remote delete')
-                    else:
-                        actions[f] = (
-                            ACTION_CHANGED_DELETED,
-                            (f, None, f, False, pa.node()),
-                            b'prompt changed/deleted',
-                        )
-                elif n1 == addednodeid:
-                    # This extra 'a' is added by working copy manifest to mark
-                    # the file as locally added. We should forget it instead of
-                    # deleting it.
-                    actions[f] = (ACTION_FORGET, None, b'remote deleted')
-                else:
-                    actions[f] = (ACTION_REMOVE, None, b'other deleted')
-        elif n2:  # file exists only on remote side
-            if f in copied1:
-                pass  # we'll deal with it on m1 side
-            elif f in branch_copies2.movewithdir:
-                f2 = branch_copies2.movewithdir[f]
-                if f2 in m1:
-                    actions[f2] = (
-                        ACTION_MERGE,
-                        (f2, f, None, False, pa.node()),
-                        b'local directory rename, both created',
-                    )
-                else:
-                    actions[f2] = (
-                        ACTION_LOCAL_DIR_RENAME_GET,
-                        (f, fl2),
-                        b'local directory rename - get from %s' % f,
-                    )
-            elif f in branch_copies2.copy:
-                f2 = branch_copies2.copy[f]
-                if f2 in m2:
-                    actions[f] = (
-                        ACTION_MERGE,
-                        (f2, f, f2, False, pa.node()),
-                        b'remote copied from %s' % f2,
-                    )
-                else:
-                    actions[f] = (
-                        ACTION_MERGE,
-                        (f2, f, f2, True, pa.node()),
-                        b'remote moved from %s' % f2,
-                    )
-            elif f not in ma:
-                # local unknown, remote created: the logic is described by the
-                # following table:
-                #
-                # force  branchmerge  different  |  action
-                #   n         *           *      |   create
-                #   y         n           *      |   create
-                #   y         y           n      |   create
-                #   y         y           y      |   merge
-                #
-                # Checking whether the files are different is expensive, so we
-                # don't do that when we can avoid it.
-                if not force:
-                    actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
-                elif not branchmerge:
-                    actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
-                else:
-                    actions[f] = (
-                        ACTION_CREATED_MERGE,
-                        (fl2, pa.node()),
-                        b'remote created, get or merge',
-                    )
-            elif n2 != ma[f]:
-                df = None
-                for d in branch_copies1.dirmove:
-                    if f.startswith(d):
-                        # new file added in a directory that was moved
-                        df = branch_copies1.dirmove[d] + f[len(d) :]
-                        break
-                if df is not None and df in m1:
-                    actions[df] = (
-                        ACTION_MERGE,
-                        (df, f, f, False, pa.node()),
-                        b'local directory rename - respect move '
-                        b'from %s' % f,
-                    )
-                elif acceptremote:
-                    actions[f] = (ACTION_CREATED, (fl2,), b'remote recreating')
-                else:
-                    actions[f] = (
-                        ACTION_DELETED_CHANGED,
-                        (None, f, f, False, pa.node()),
-                        b'prompt deleted/changed',
-                    )
-
-    if repo.ui.configbool(b'experimental', b'merge.checkpathconflicts'):
-        # If we are merging, look for path conflicts.
-        checkpathconflicts(repo, wctx, p2, actions)
-
-    narrowmatch = repo.narrowmatch()
-    if not narrowmatch.always():
-        # Updates "actions" in place
-        _filternarrowactions(narrowmatch, branchmerge, actions)
-
-    renamedelete = branch_copies1.renamedelete
-    renamedelete.update(branch_copies2.renamedelete)
-
-    return actions, diverge, renamedelete
-
-
-def _resolvetrivial(repo, wctx, mctx, ancestor, actions):
-    """Resolves false conflicts where the nodeid changed but the content
-       remained the same."""
-    # We force a copy of actions.items() because we're going to mutate
-    # actions as we resolve trivial conflicts.
-    for f, (m, args, msg) in list(actions.items()):
-        if (
-            m == ACTION_CHANGED_DELETED
-            and f in ancestor
-            and not wctx[f].cmp(ancestor[f])
-        ):
-            # local did change but ended up with same content
-            actions[f] = ACTION_REMOVE, None, b'prompt same'
-        elif (
-            m == ACTION_DELETED_CHANGED
-            and f in ancestor
-            and not mctx[f].cmp(ancestor[f])
-        ):
-            # remote did change but ended up with same content
-            del actions[f]  # don't get = keep local deleted
-
-
-def calculateupdates(
-    repo,
-    wctx,
-    mctx,
-    ancestors,
-    branchmerge,
-    force,
-    acceptremote,
-    followcopies,
-    matcher=None,
-    mergeforce=False,
-):
-    """Calculate the actions needed to merge mctx into wctx using ancestors"""
-    # Avoid cycle.
-    from . import sparse
-
-    if len(ancestors) == 1:  # default
-        actions, diverge, renamedelete = manifestmerge(
-            repo,
-            wctx,
-            mctx,
-            ancestors[0],
-            branchmerge,
-            force,
-            matcher,
-            acceptremote,
-            followcopies,
-        )
-        _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
-
-    else:  # only when merge.preferancestor=* - the default
-        repo.ui.note(
-            _(b"note: merging %s and %s using bids from ancestors %s\n")
-            % (
-                wctx,
-                mctx,
-                _(b' and ').join(pycompat.bytestr(anc) for anc in ancestors),
-            )
-        )
-
-        # Call for bids
-        fbids = (
-            {}
-        )  # mapping filename to bids (action method to list af actions)
-        diverge, renamedelete = None, None
-        for ancestor in ancestors:
-            repo.ui.note(_(b'\ncalculating bids for ancestor %s\n') % ancestor)
-            actions, diverge1, renamedelete1 = manifestmerge(
-                repo,
-                wctx,
-                mctx,
-                ancestor,
-                branchmerge,
-                force,
-                matcher,
-                acceptremote,
-                followcopies,
-                forcefulldiff=True,
-            )
-            _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
-
-            # Track the shortest set of warning on the theory that bid
-            # merge will correctly incorporate more information
-            if diverge is None or len(diverge1) < len(diverge):
-                diverge = diverge1
-            if renamedelete is None or len(renamedelete) < len(renamedelete1):
-                renamedelete = renamedelete1
-
-            for f, a in sorted(pycompat.iteritems(actions)):
-                m, args, msg = a
-                if m == ACTION_GET_OTHER_AND_STORE:
-                    m = ACTION_GET
-                repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m))
-                if f in fbids:
-                    d = fbids[f]
-                    if m in d:
-                        d[m].append(a)
-                    else:
-                        d[m] = [a]
-                else:
-                    fbids[f] = {m: [a]}
-
-        # Pick the best bid for each file
-        repo.ui.note(_(b'\nauction for merging merge bids\n'))
-        actions = {}
-        for f, bids in sorted(fbids.items()):
-            # bids is a mapping from action method to list af actions
-            # Consensus?
-            if len(bids) == 1:  # all bids are the same kind of method
-                m, l = list(bids.items())[0]
-                if all(a == l[0] for a in l[1:]):  # len(bids) is > 1
-                    repo.ui.note(_(b" %s: consensus for %s\n") % (f, m))
-                    actions[f] = l[0]
-                    continue
-            # If keep is an option, just do it.
-            if ACTION_KEEP in bids:
-                repo.ui.note(_(b" %s: picking 'keep' action\n") % f)
-                actions[f] = bids[ACTION_KEEP][0]
-                continue
-            # If there are gets and they all agree [how could they not?], do it.
-            if ACTION_GET in bids:
-                ga0 = bids[ACTION_GET][0]
-                if all(a == ga0 for a in bids[ACTION_GET][1:]):
-                    repo.ui.note(_(b" %s: picking 'get' action\n") % f)
-                    actions[f] = ga0
-                    continue
-            # TODO: Consider other simple actions such as mode changes
-            # Handle inefficient democrazy.
-            repo.ui.note(_(b' %s: multiple bids for merge action:\n') % f)
-            for m, l in sorted(bids.items()):
-                for _f, args, msg in l:
-                    repo.ui.note(b'  %s -> %s\n' % (msg, m))
-            # Pick random action. TODO: Instead, prompt user when resolving
-            m, l = list(bids.items())[0]
-            repo.ui.warn(
-                _(b' %s: ambiguous merge - picked %s action\n') % (f, m)
-            )
-            actions[f] = l[0]
-            continue
-        repo.ui.note(_(b'end of auction\n\n'))
-
-    if wctx.rev() is None:
-        fractions = _forgetremoved(wctx, mctx, branchmerge)
-        actions.update(fractions)
-
-    prunedactions = sparse.filterupdatesactions(
-        repo, wctx, mctx, branchmerge, actions
-    )
-    _resolvetrivial(repo, wctx, mctx, ancestors[0], actions)
-
-    return prunedactions, diverge, renamedelete
-
-
-def _getcwd():
-    try:
-        return encoding.getcwd()
-    except OSError as err:
-        if err.errno == errno.ENOENT:
-            return None
-        raise
-
-
-def batchremove(repo, wctx, actions):
-    """apply removes to the working directory
-
-    yields tuples for progress updates
-    """
-    verbose = repo.ui.verbose
-    cwd = _getcwd()
-    i = 0
-    for f, args, msg in actions:
-        repo.ui.debug(b" %s: %s -> r\n" % (f, msg))
-        if verbose:
-            repo.ui.note(_(b"removing %s\n") % f)
-        wctx[f].audit()
-        try:
-            wctx[f].remove(ignoremissing=True)
-        except OSError as inst:
-            repo.ui.warn(
-                _(b"update failed to remove %s: %s!\n") % (f, inst.strerror)
-            )
-        if i == 100:
-            yield i, f
-            i = 0
-        i += 1
-    if i > 0:
-        yield i, f
-
-    if cwd and not _getcwd():
-        # cwd was removed in the course of removing files; print a helpful
-        # warning.
-        repo.ui.warn(
-            _(
-                b"current directory was removed\n"
-                b"(consider changing to repo root: %s)\n"
-            )
-            % repo.root
-        )
-
-
-def batchget(repo, mctx, wctx, wantfiledata, actions):
-    """apply gets to the working directory
-
-    mctx is the context to get from
-
-    Yields arbitrarily many (False, tuple) for progress updates, followed by
-    exactly one (True, filedata). When wantfiledata is false, filedata is an
-    empty dict. When wantfiledata is true, filedata[f] is a triple (mode, size,
-    mtime) of the file f written for each action.
-    """
-    filedata = {}
-    verbose = repo.ui.verbose
-    fctx = mctx.filectx
-    ui = repo.ui
-    i = 0
-    with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
-        for f, (flags, backup), msg in actions:
-            repo.ui.debug(b" %s: %s -> g\n" % (f, msg))
-            if verbose:
-                repo.ui.note(_(b"getting %s\n") % f)
-
-            if backup:
-                # If a file or directory exists with the same name, back that
-                # up.  Otherwise, look to see if there is a file that conflicts
-                # with a directory this file is in, and if so, back that up.
-                conflicting = f
-                if not repo.wvfs.lexists(f):
-                    for p in pathutil.finddirs(f):
-                        if repo.wvfs.isfileorlink(p):
-                            conflicting = p
-                            break
-                if repo.wvfs.lexists(conflicting):
-                    orig = scmutil.backuppath(ui, repo, conflicting)
-                    util.rename(repo.wjoin(conflicting), orig)
-            wfctx = wctx[f]
-            wfctx.clearunknown()
-            atomictemp = ui.configbool(b"experimental", b"update.atomic-file")
-            size = wfctx.write(
-                fctx(f).data(),
-                flags,
-                backgroundclose=True,
-                atomictemp=atomictemp,
-            )
-            if wantfiledata:
-                s = wfctx.lstat()
-                mode = s.st_mode
-                mtime = s[stat.ST_MTIME]
-                filedata[f] = (mode, size, mtime)  # for dirstate.normal
-            if i == 100:
-                yield False, (i, f)
-                i = 0
-            i += 1
-    if i > 0:
-        yield False, (i, f)
-    yield True, filedata
-
-
-def _prefetchfiles(repo, ctx, actions):
-    """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
-    of merge actions.  ``ctx`` is the context being merged in."""
-
-    # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
-    # don't touch the context to be merged in.  'cd' is skipped, because
-    # changed/deleted never resolves to something from the remote side.
-    oplist = [
-        actions[a]
-        for a in (
-            ACTION_GET,
-            ACTION_DELETED_CHANGED,
-            ACTION_LOCAL_DIR_RENAME_GET,
-            ACTION_MERGE,
-        )
-    ]
-    prefetch = scmutil.prefetchfiles
-    matchfiles = scmutil.matchfiles
-    prefetch(
-        repo,
-        [ctx.rev()],
-        matchfiles(repo, [f for sublist in oplist for f, args, msg in sublist]),
-    )
-
-
- at attr.s(frozen=True)
-class updateresult(object):
-    updatedcount = attr.ib()
-    mergedcount = attr.ib()
-    removedcount = attr.ib()
-    unresolvedcount = attr.ib()
-
-    def isempty(self):
-        return not (
-            self.updatedcount
-            or self.mergedcount
-            or self.removedcount
-            or self.unresolvedcount
-        )
-
-
-def emptyactions():
-    """create an actions dict, to be populated and passed to applyupdates()"""
-    return {
-        m: []
-        for m in (
-            ACTION_ADD,
-            ACTION_ADD_MODIFIED,
-            ACTION_FORGET,
-            ACTION_GET,
-            ACTION_CHANGED_DELETED,
-            ACTION_DELETED_CHANGED,
-            ACTION_REMOVE,
-            ACTION_DIR_RENAME_MOVE_LOCAL,
-            ACTION_LOCAL_DIR_RENAME_GET,
-            ACTION_MERGE,
-            ACTION_EXEC,
-            ACTION_KEEP,
-            ACTION_PATH_CONFLICT,
-            ACTION_PATH_CONFLICT_RESOLVE,
-            ACTION_GET_OTHER_AND_STORE,
-        )
-    }
-
-
-def applyupdates(
-    repo, actions, wctx, mctx, overwrite, wantfiledata, labels=None
-):
-    """apply the merge action list to the working directory
-
-    wctx is the working copy context
-    mctx is the context to be merged into the working copy
-
-    Return a tuple of (counts, filedata), where counts is a tuple
-    (updated, merged, removed, unresolved) that describes how many
-    files were affected by the update, and filedata is as described in
-    batchget.
-    """
-
-    _prefetchfiles(repo, mctx, actions)
-
-    updated, merged, removed = 0, 0, 0
-    ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels)
-
-    # add ACTION_GET_OTHER_AND_STORE to mergestate
-    for e in actions[ACTION_GET_OTHER_AND_STORE]:
-        ms.addmergedother(e[0])
-
-    moves = []
-    for m, l in actions.items():
-        l.sort()
-
-    # 'cd' and 'dc' actions are treated like other merge conflicts
-    mergeactions = sorted(actions[ACTION_CHANGED_DELETED])
-    mergeactions.extend(sorted(actions[ACTION_DELETED_CHANGED]))
-    mergeactions.extend(actions[ACTION_MERGE])
-    for f, args, msg in mergeactions:
-        f1, f2, fa, move, anc = args
-        if f == b'.hgsubstate':  # merged internally
-            continue
-        if f1 is None:
-            fcl = filemerge.absentfilectx(wctx, fa)
-        else:
-            repo.ui.debug(b" preserving %s for resolve of %s\n" % (f1, f))
-            fcl = wctx[f1]
-        if f2 is None:
-            fco = filemerge.absentfilectx(mctx, fa)
-        else:
-            fco = mctx[f2]
-        actx = repo[anc]
-        if fa in actx:
-            fca = actx[fa]
-        else:
-            # TODO: move to absentfilectx
-            fca = repo.filectx(f1, fileid=nullrev)
-        ms.add(fcl, fco, fca, f)
-        if f1 != f and move:
-            moves.append(f1)
-
-    # remove renamed files after safely stored
-    for f in moves:
-        if wctx[f].lexists():
-            repo.ui.debug(b"removing %s\n" % f)
-            wctx[f].audit()
-            wctx[f].remove()
-
-    numupdates = sum(len(l) for m, l in actions.items() if m != ACTION_KEEP)
-    progress = repo.ui.makeprogress(
-        _(b'updating'), unit=_(b'files'), total=numupdates
-    )
-
-    if [a for a in actions[ACTION_REMOVE] if a[0] == b'.hgsubstate']:
-        subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
-
-    # record path conflicts
-    for f, args, msg in actions[ACTION_PATH_CONFLICT]:
-        f1, fo = args
-        s = repo.ui.status
-        s(
-            _(
-                b"%s: path conflict - a file or link has the same name as a "
-                b"directory\n"
-            )
-            % f
-        )
-        if fo == b'l':
-            s(_(b"the local file has been renamed to %s\n") % f1)
-        else:
-            s(_(b"the remote file has been renamed to %s\n") % f1)
-        s(_(b"resolve manually then use 'hg resolve --mark %s'\n") % f)
-        ms.addpath(f, f1, fo)
-        progress.increment(item=f)
-
-    # When merging in-memory, we can't support worker processes, so set the
-    # per-item cost at 0 in that case.
-    cost = 0 if wctx.isinmemory() else 0.001
-
-    # remove in parallel (must come before resolving path conflicts and getting)
-    prog = worker.worker(
-        repo.ui, cost, batchremove, (repo, wctx), actions[ACTION_REMOVE]
-    )
-    for i, item in prog:
-        progress.increment(step=i, item=item)
-    removed = len(actions[ACTION_REMOVE])
-
-    # resolve path conflicts (must come before getting)
-    for f, args, msg in actions[ACTION_PATH_CONFLICT_RESOLVE]:
-        repo.ui.debug(b" %s: %s -> pr\n" % (f, msg))
-        (f0,) = args
-        if wctx[f0].lexists():
-            repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
-            wctx[f].audit()
-            wctx[f].write(wctx.filectx(f0).data(), wctx.filectx(f0).flags())
-            wctx[f0].remove()
-        progress.increment(item=f)
-
-    # get in parallel.
-    threadsafe = repo.ui.configbool(
-        b'experimental', b'worker.wdir-get-thread-safe'
-    )
-    prog = worker.worker(
-        repo.ui,
-        cost,
-        batchget,
-        (repo, mctx, wctx, wantfiledata),
-        actions[ACTION_GET],
-        threadsafe=threadsafe,
-        hasretval=True,
-    )
-    getfiledata = {}
-    for final, res in prog:
-        if final:
-            getfiledata = res
-        else:
-            i, item = res
-            progress.increment(step=i, item=item)
-    updated = len(actions[ACTION_GET])
-
-    if [a for a in actions[ACTION_GET] if a[0] == b'.hgsubstate']:
-        subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
-
-    # forget (manifest only, just log it) (must come first)
-    for f, args, msg in actions[ACTION_FORGET]:
-        repo.ui.debug(b" %s: %s -> f\n" % (f, msg))
-        progress.increment(item=f)
-
-    # re-add (manifest only, just log it)
-    for f, args, msg in actions[ACTION_ADD]:
-        repo.ui.debug(b" %s: %s -> a\n" % (f, msg))
-        progress.increment(item=f)
-
-    # re-add/mark as modified (manifest only, just log it)
-    for f, args, msg in actions[ACTION_ADD_MODIFIED]:
-        repo.ui.debug(b" %s: %s -> am\n" % (f, msg))
-        progress.increment(item=f)
-
-    # keep (noop, just log it)
-    for f, args, msg in actions[ACTION_KEEP]:
-        repo.ui.debug(b" %s: %s -> k\n" % (f, msg))
-        # no progress
-
-    # directory rename, move local
-    for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
-        repo.ui.debug(b" %s: %s -> dm\n" % (f, msg))
-        progress.increment(item=f)
-        f0, flags = args
-        repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
-        wctx[f].audit()
-        wctx[f].write(wctx.filectx(f0).data(), flags)
-        wctx[f0].remove()
-        updated += 1
-
-    # local directory rename, get
-    for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
-        repo.ui.debug(b" %s: %s -> dg\n" % (f, msg))
-        progress.increment(item=f)
-        f0, flags = args
-        repo.ui.note(_(b"getting %s to %s\n") % (f0, f))
-        wctx[f].write(mctx.filectx(f0).data(), flags)
-        updated += 1
-
-    # exec
-    for f, args, msg in actions[ACTION_EXEC]:
-        repo.ui.debug(b" %s: %s -> e\n" % (f, msg))
-        progress.increment(item=f)
-        (flags,) = args
-        wctx[f].audit()
-        wctx[f].setflags(b'l' in flags, b'x' in flags)
-        updated += 1
-
-    # the ordering is important here -- ms.mergedriver will raise if the merge
-    # driver has changed, and we want to be able to bypass it when overwrite is
-    # True
-    usemergedriver = not overwrite and mergeactions and ms.mergedriver
-
-    if usemergedriver:
-        if wctx.isinmemory():
-            raise error.InMemoryMergeConflictsError(
-                b"in-memory merge does not support mergedriver"
-            )
-        ms.commit()
-        proceed = driverpreprocess(repo, ms, wctx, labels=labels)
-        # the driver might leave some files unresolved
-        unresolvedf = set(ms.unresolved())
-        if not proceed:
-            # XXX setting unresolved to at least 1 is a hack to make sure we
-            # error out
-            return updateresult(
-                updated, merged, removed, max(len(unresolvedf), 1)
-            )
-        newactions = []
-        for f, args, msg in mergeactions:
-            if f in unresolvedf:
-                newactions.append((f, args, msg))
-        mergeactions = newactions
-
-    try:
-        # premerge
-        tocomplete = []
-        for f, args, msg in mergeactions:
-            repo.ui.debug(b" %s: %s -> m (premerge)\n" % (f, msg))
-            progress.increment(item=f)
-            if f == b'.hgsubstate':  # subrepo states need updating
-                subrepoutil.submerge(
-                    repo, wctx, mctx, wctx.ancestor(mctx), overwrite, labels
-                )
-                continue
-            wctx[f].audit()
-            complete, r = ms.preresolve(f, wctx)
-            if not complete:
-                numupdates += 1
-                tocomplete.append((f, args, msg))
-
-        # merge
-        for f, args, msg in tocomplete:
-            repo.ui.debug(b" %s: %s -> m (merge)\n" % (f, msg))
-            progress.increment(item=f, total=numupdates)
-            ms.resolve(f, wctx)
-
-    finally:
-        ms.commit()
-
-    unresolved = ms.unresolvedcount()
-
-    if (
-        usemergedriver
-        and not unresolved
-        and ms.mdstate() != MERGE_DRIVER_STATE_SUCCESS
-    ):
-        if not driverconclude(repo, ms, wctx, labels=labels):
-            # XXX setting unresolved to at least 1 is a hack to make sure we
-            # error out
-            unresolved = max(unresolved, 1)
-
-        ms.commit()
-
-    msupdated, msmerged, msremoved = ms.counts()
-    updated += msupdated
-    merged += msmerged
-    removed += msremoved
-
-    extraactions = ms.actions()
-    if extraactions:
-        mfiles = {a[0] for a in actions[ACTION_MERGE]}
-        for k, acts in pycompat.iteritems(extraactions):
-            actions[k].extend(acts)
-            if k == ACTION_GET and wantfiledata:
-                # no filedata until mergestate is updated to provide it
-                for a in acts:
-                    getfiledata[a[0]] = None
-            # Remove these files from actions[ACTION_MERGE] as well. This is
-            # important because in recordupdates, files in actions[ACTION_MERGE]
-            # are processed after files in other actions, and the merge driver
-            # might add files to those actions via extraactions above. This can
-            # lead to a file being recorded twice, with poor results. This is
-            # especially problematic for actions[ACTION_REMOVE] (currently only
-            # possible with the merge driver in the initial merge process;
-            # interrupted merges don't go through this flow).
-            #
-            # The real fix here is to have indexes by both file and action so
-            # that when the action for a file is changed it is automatically
-            # reflected in the other action lists. But that involves a more
-            # complex data structure, so this will do for now.
-            #
-            # We don't need to do the same operation for 'dc' and 'cd' because
-            # those lists aren't consulted again.
-            mfiles.difference_update(a[0] for a in acts)
-
-        actions[ACTION_MERGE] = [
-            a for a in actions[ACTION_MERGE] if a[0] in mfiles
-        ]
-
-    progress.complete()
-    assert len(getfiledata) == (len(actions[ACTION_GET]) if wantfiledata else 0)
-    return updateresult(updated, merged, removed, unresolved), getfiledata
-
-
 def recordupdates(repo, actions, branchmerge, getfiledata):
     """record merge actions to the dirstate"""
     # remove (must come first)
@@ -2232,594 +848,3 @@
             repo.dirstate.copy(f0, f)
         else:
             repo.dirstate.normal(f)
-
-
-UPDATECHECK_ABORT = b'abort'  # handled at higher layers
-UPDATECHECK_NONE = b'none'
-UPDATECHECK_LINEAR = b'linear'
-UPDATECHECK_NO_CONFLICT = b'noconflict'
-
-
-def update(
-    repo,
-    node,
-    branchmerge,
-    force,
-    ancestor=None,
-    mergeancestor=False,
-    labels=None,
-    matcher=None,
-    mergeforce=False,
-    updatedirstate=True,
-    updatecheck=None,
-    wc=None,
-):
-    """
-    Perform a merge between the working directory and the given node
-
-    node = the node to update to
-    branchmerge = whether to merge between branches
-    force = whether to force branch merging or file overwriting
-    matcher = a matcher to filter file lists (dirstate not updated)
-    mergeancestor = whether it is merging with an ancestor. If true,
-      we should accept the incoming changes for any prompts that occur.
-      If false, merging with an ancestor (fast-forward) is only allowed
-      between different named branches. This flag is used by rebase extension
-      as a temporary fix and should be avoided in general.
-    labels = labels to use for base, local and other
-    mergeforce = whether the merge was run with 'merge --force' (deprecated): if
-      this is True, then 'force' should be True as well.
-
-    The table below shows all the behaviors of the update command given the
-    -c/--check and -C/--clean or no options, whether the working directory is
-    dirty, whether a revision is specified, and the relationship of the parent
-    rev to the target rev (linear or not). Match from top first. The -n
-    option doesn't exist on the command line, but represents the
-    experimental.updatecheck=noconflict option.
-
-    This logic is tested by test-update-branches.t.
-
-    -c  -C  -n  -m  dirty  rev  linear  |  result
-     y   y   *   *    *     *     *     |    (1)
-     y   *   y   *    *     *     *     |    (1)
-     y   *   *   y    *     *     *     |    (1)
-     *   y   y   *    *     *     *     |    (1)
-     *   y   *   y    *     *     *     |    (1)
-     *   *   y   y    *     *     *     |    (1)
-     *   *   *   *    *     n     n     |     x
-     *   *   *   *    n     *     *     |    ok
-     n   n   n   n    y     *     y     |   merge
-     n   n   n   n    y     y     n     |    (2)
-     n   n   n   y    y     *     *     |   merge
-     n   n   y   n    y     *     *     |  merge if no conflict
-     n   y   n   n    y     *     *     |  discard
-     y   n   n   n    y     *     *     |    (3)
-
-    x = can't happen
-    * = don't-care
-    1 = incompatible options (checked in commands.py)
-    2 = abort: uncommitted changes (commit or update --clean to discard changes)
-    3 = abort: uncommitted changes (checked in commands.py)
-
-    The merge is performed inside ``wc``, a workingctx-like objects. It defaults
-    to repo[None] if None is passed.
-
-    Return the same tuple as applyupdates().
-    """
-    # Avoid cycle.
-    from . import sparse
-
-    # This function used to find the default destination if node was None, but
-    # that's now in destutil.py.
-    assert node is not None
-    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 = UPDATECHECK_LINEAR
-        if updatecheck not in (
-            UPDATECHECK_NONE,
-            UPDATECHECK_LINEAR,
-            UPDATECHECK_NO_CONFLICT,
-        ):
-            raise ValueError(
-                r'Invalid updatecheck %r (can accept %r)'
-                % (
-                    updatecheck,
-                    (
-                        UPDATECHECK_NONE,
-                        UPDATECHECK_LINEAR,
-                        UPDATECHECK_NO_CONFLICT,
-                    ),
-                )
-            )
-    with repo.wlock():
-        if wc is None:
-            wc = repo[None]
-        pl = wc.parents()
-        p1 = pl[0]
-        p2 = repo[node]
-        if ancestor is not None:
-            pas = [repo[ancestor]]
-        else:
-            if repo.ui.configlist(b'merge', b'preferancestor') == [b'*']:
-                cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
-                pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
-            else:
-                pas = [p1.ancestor(p2, warn=branchmerge)]
-
-        fp1, fp2, xp1, xp2 = p1.node(), p2.node(), bytes(p1), bytes(p2)
-
-        overwrite = force and not branchmerge
-        ### check phase
-        if not overwrite:
-            if len(pl) > 1:
-                raise error.Abort(_(b"outstanding uncommitted merge"))
-            ms = mergestate.read(repo)
-            if list(ms.unresolved()):
-                raise error.Abort(
-                    _(b"outstanding merge conflicts"),
-                    hint=_(b"use 'hg resolve' to resolve"),
-                )
-        if branchmerge:
-            if pas == [p2]:
-                raise error.Abort(
-                    _(
-                        b"merging with a working directory ancestor"
-                        b" has no effect"
-                    )
-                )
-            elif pas == [p1]:
-                if not mergeancestor and wc.branch() == p2.branch():
-                    raise error.Abort(
-                        _(b"nothing to merge"),
-                        hint=_(b"use 'hg update' or check 'hg heads'"),
-                    )
-            if not force and (wc.files() or wc.deleted()):
-                raise error.Abort(
-                    _(b"uncommitted changes"),
-                    hint=_(b"use 'hg status' to list changes"),
-                )
-            if not wc.isinmemory():
-                for s in sorted(wc.substate):
-                    wc.sub(s).bailifchanged()
-
-        elif not overwrite:
-            if p1 == p2:  # no-op update
-                # call the hooks and exit early
-                repo.hook(b'preupdate', throw=True, parent1=xp2, parent2=b'')
-                repo.hook(b'update', parent1=xp2, parent2=b'', error=0)
-                return updateresult(0, 0, 0, 0)
-
-            if updatecheck == UPDATECHECK_LINEAR and pas not in (
-                [p1],
-                [p2],
-            ):  # nonlinear
-                dirty = wc.dirty(missing=True)
-                if dirty:
-                    # Branching is a bit strange to ensure we do the minimal
-                    # amount of call to obsutil.foreground.
-                    foreground = obsutil.foreground(repo, [p1.node()])
-                    # note: the <node> variable contains a random identifier
-                    if repo[node].node() in foreground:
-                        pass  # allow updating to successors
-                    else:
-                        msg = _(b"uncommitted changes")
-                        hint = _(b"commit or update --clean to discard changes")
-                        raise error.UpdateAbort(msg, hint=hint)
-                else:
-                    # Allow jumping branches if clean and specific rev given
-                    pass
-
-        if overwrite:
-            pas = [wc]
-        elif not branchmerge:
-            pas = [p1]
-
-        # deprecated config: merge.followcopies
-        followcopies = repo.ui.configbool(b'merge', b'followcopies')
-        if overwrite:
-            followcopies = False
-        elif not pas[0]:
-            followcopies = False
-        if not branchmerge and not wc.dirty(missing=True):
-            followcopies = False
-
-        ### calculate phase
-        actionbyfile, diverge, renamedelete = calculateupdates(
-            repo,
-            wc,
-            p2,
-            pas,
-            branchmerge,
-            force,
-            mergeancestor,
-            followcopies,
-            matcher=matcher,
-            mergeforce=mergeforce,
-        )
-
-        if updatecheck == UPDATECHECK_NO_CONFLICT:
-            for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
-                if m not in (
-                    ACTION_GET,
-                    ACTION_KEEP,
-                    ACTION_EXEC,
-                    ACTION_REMOVE,
-                    ACTION_PATH_CONFLICT_RESOLVE,
-                    ACTION_GET_OTHER_AND_STORE,
-                ):
-                    msg = _(b"conflicting changes")
-                    hint = _(b"commit or update --clean to discard changes")
-                    raise error.Abort(msg, hint=hint)
-
-        # Prompt and create actions. Most of this is in the resolve phase
-        # already, but we can't handle .hgsubstate in filemerge or
-        # subrepoutil.submerge yet so we have to keep prompting for it.
-        if b'.hgsubstate' in actionbyfile:
-            f = b'.hgsubstate'
-            m, args, msg = actionbyfile[f]
-            prompts = filemerge.partextras(labels)
-            prompts[b'f'] = f
-            if m == ACTION_CHANGED_DELETED:
-                if repo.ui.promptchoice(
-                    _(
-                        b"local%(l)s changed %(f)s which other%(o)s deleted\n"
-                        b"use (c)hanged version or (d)elete?"
-                        b"$$ &Changed $$ &Delete"
-                    )
-                    % prompts,
-                    0,
-                ):
-                    actionbyfile[f] = (ACTION_REMOVE, None, b'prompt delete')
-                elif f in p1:
-                    actionbyfile[f] = (
-                        ACTION_ADD_MODIFIED,
-                        None,
-                        b'prompt keep',
-                    )
-                else:
-                    actionbyfile[f] = (ACTION_ADD, None, b'prompt keep')
-            elif m == ACTION_DELETED_CHANGED:
-                f1, f2, fa, move, anc = args
-                flags = p2[f2].flags()
-                if (
-                    repo.ui.promptchoice(
-                        _(
-                            b"other%(o)s changed %(f)s which local%(l)s deleted\n"
-                            b"use (c)hanged version or leave (d)eleted?"
-                            b"$$ &Changed $$ &Deleted"
-                        )
-                        % prompts,
-                        0,
-                    )
-                    == 0
-                ):
-                    actionbyfile[f] = (
-                        ACTION_GET,
-                        (flags, False),
-                        b'prompt recreating',
-                    )
-                else:
-                    del actionbyfile[f]
-
-        # Convert to dictionary-of-lists format
-        actions = emptyactions()
-        for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
-            if m not in actions:
-                actions[m] = []
-            actions[m].append((f, args, msg))
-
-        # ACTION_GET_OTHER_AND_STORE is a ACTION_GET + store in mergestate
-        for e in actions[ACTION_GET_OTHER_AND_STORE]:
-            actions[ACTION_GET].append(e)
-
-        if not util.fscasesensitive(repo.path):
-            # check collision between files only in p2 for clean update
-            if not branchmerge and (
-                force or not wc.dirty(missing=True, branch=False)
-            ):
-                _checkcollision(repo, p2.manifest(), None)
-            else:
-                _checkcollision(repo, wc.manifest(), actions)
-
-        # divergent renames
-        for f, fl in sorted(pycompat.iteritems(diverge)):
-            repo.ui.warn(
-                _(
-                    b"note: possible conflict - %s was renamed "
-                    b"multiple times to:\n"
-                )
-                % f
-            )
-            for nf in sorted(fl):
-                repo.ui.warn(b" %s\n" % nf)
-
-        # rename and delete
-        for f, fl in sorted(pycompat.iteritems(renamedelete)):
-            repo.ui.warn(
-                _(
-                    b"note: possible conflict - %s was deleted "
-                    b"and renamed to:\n"
-                )
-                % f
-            )
-            for nf in sorted(fl):
-                repo.ui.warn(b" %s\n" % nf)
-
-        ### apply phase
-        if not branchmerge:  # just jump to the new rev
-            fp1, fp2, xp1, xp2 = fp2, nullid, xp2, b''
-        # If we're doing a partial update, we need to skip updating
-        # the dirstate.
-        always = matcher is None or matcher.always()
-        updatedirstate = updatedirstate and always and not wc.isinmemory()
-        if updatedirstate:
-            repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
-            # note that we're in the middle of an update
-            repo.vfs.write(b'updatestate', p2.hex())
-
-        # Advertise fsmonitor when its presence could be useful.
-        #
-        # We only advertise when performing an update from an empty working
-        # directory. This typically only occurs during initial clone.
-        #
-        # We give users a mechanism to disable the warning in case it is
-        # annoying.
-        #
-        # We only allow on Linux and MacOS because that's where fsmonitor is
-        # considered stable.
-        fsmonitorwarning = repo.ui.configbool(b'fsmonitor', b'warn_when_unused')
-        fsmonitorthreshold = repo.ui.configint(
-            b'fsmonitor', b'warn_update_file_count'
-        )
-        try:
-            # avoid cycle: extensions -> cmdutil -> merge
-            from . import extensions
-
-            extensions.find(b'fsmonitor')
-            fsmonitorenabled = repo.ui.config(b'fsmonitor', b'mode') != b'off'
-            # We intentionally don't look at whether fsmonitor has disabled
-            # itself because a) fsmonitor may have already printed a warning
-            # b) we only care about the config state here.
-        except KeyError:
-            fsmonitorenabled = False
-
-        if (
-            fsmonitorwarning
-            and not fsmonitorenabled
-            and p1.node() == nullid
-            and len(actions[ACTION_GET]) >= fsmonitorthreshold
-            and pycompat.sysplatform.startswith((b'linux', b'darwin'))
-        ):
-            repo.ui.warn(
-                _(
-                    b'(warning: large working directory being used without '
-                    b'fsmonitor enabled; enable fsmonitor to improve performance; '
-                    b'see "hg help -e fsmonitor")\n'
-                )
-            )
-
-        wantfiledata = updatedirstate and not branchmerge
-        stats, getfiledata = applyupdates(
-            repo, actions, wc, p2, overwrite, wantfiledata, labels=labels
-        )
-
-        if updatedirstate:
-            with repo.dirstate.parentchange():
-                repo.setparents(fp1, fp2)
-                recordupdates(repo, actions, branchmerge, getfiledata)
-                # update completed, clear state
-                util.unlink(repo.vfs.join(b'updatestate'))
-
-                if not branchmerge:
-                    repo.dirstate.setbranch(p2.branch())
-
-    # If we're updating to a location, clean up any stale temporary includes
-    # (ex: this happens during hg rebase --abort).
-    if not branchmerge:
-        sparse.prunetemporaryincludes(repo)
-
-    if updatedirstate:
-        repo.hook(
-            b'update', parent1=xp1, parent2=xp2, error=stats.unresolvedcount
-        )
-    return stats
-
-
-def merge(ctx, labels=None, force=False, wc=None):
-    """Merge another topological branch into the working copy.
-
-    force = whether the merge was run with 'merge --force' (deprecated)
-    """
-
-    return update(
-        ctx.repo(),
-        ctx.rev(),
-        labels=labels,
-        branchmerge=True,
-        force=force,
-        mergeforce=force,
-        wc=wc,
-    )
-
-
-def clean_update(ctx, wc=None):
-    """Do a clean update to the given commit.
-
-    This involves updating to the commit and discarding any changes in the
-    working copy.
-    """
-    return update(ctx.repo(), ctx.rev(), branchmerge=False, force=True, wc=wc)
-
-
-def revert_to(ctx, matcher=None, wc=None):
-    """Revert the working copy to the given commit.
-
-    The working copy will keep its current parent(s) but its content will
-    be the same as in the given commit.
-    """
-
-    return update(
-        ctx.repo(),
-        ctx.rev(),
-        branchmerge=False,
-        force=True,
-        updatedirstate=False,
-        matcher=matcher,
-        wc=wc,
-    )
-
-
-def graft(
-    repo,
-    ctx,
-    base=None,
-    labels=None,
-    keepparent=False,
-    keepconflictparent=False,
-    wctx=None,
-):
-    """Do a graft-like merge.
-
-    This is a merge where the merge ancestor is chosen such that one
-    or more changesets are grafted onto the current changeset. In
-    addition to the merge, this fixes up the dirstate to include only
-    a single parent (if keepparent is False) and tries to duplicate any
-    renames/copies appropriately.
-
-    ctx - changeset to rebase
-    base - merge base, or ctx.p1() if not specified
-    labels - merge labels eg ['local', 'graft']
-    keepparent - keep second parent if any
-    keepconflictparent - if unresolved, keep parent used for the merge
-
-    """
-    # If we're grafting a descendant onto an ancestor, be sure to pass
-    # mergeancestor=True to update. This does two things: 1) allows the merge if
-    # the destination is the same as the parent of the ctx (so we can use graft
-    # to copy commits), and 2) informs update that the incoming changes are
-    # newer than the destination so it doesn't prompt about "remote changed foo
-    # which local deleted".
-    # We also pass mergeancestor=True when base is the same revision as p1. 2)
-    # doesn't matter as there can't possibly be conflicts, but 1) is necessary.
-    wctx = wctx or repo[None]
-    pctx = wctx.p1()
-    base = base or ctx.p1()
-    mergeancestor = (
-        repo.changelog.isancestor(pctx.node(), ctx.node())
-        or pctx.rev() == base.rev()
-    )
-
-    stats = update(
-        repo,
-        ctx.node(),
-        True,
-        True,
-        base.node(),
-        mergeancestor=mergeancestor,
-        labels=labels,
-        wc=wctx,
-    )
-
-    if keepconflictparent and stats.unresolvedcount:
-        pother = ctx.node()
-    else:
-        pother = nullid
-        parents = ctx.parents()
-        if keepparent and len(parents) == 2 and base in parents:
-            parents.remove(base)
-            pother = parents[0].node()
-    # Never set both parents equal to each other
-    if pother == pctx.node():
-        pother = nullid
-
-    if wctx.isinmemory():
-        wctx.setparents(pctx.node(), pother)
-        # fix up dirstate for copies and renames
-        copies.graftcopies(wctx, ctx, base)
-    else:
-        with repo.dirstate.parentchange():
-            repo.setparents(pctx.node(), pother)
-            repo.dirstate.write(repo.currenttransaction())
-            # fix up dirstate for copies and renames
-            copies.graftcopies(wctx, ctx, base)
-    return stats
-
-
-def purge(
-    repo,
-    matcher,
-    unknown=True,
-    ignored=False,
-    removeemptydirs=True,
-    removefiles=True,
-    abortonerror=False,
-    noop=False,
-):
-    """Purge the working directory of untracked files.
-
-    ``matcher`` is a matcher configured to scan the working directory -
-    potentially a subset.
-
-    ``unknown`` controls whether unknown files should be purged.
-
-    ``ignored`` controls whether ignored files should be purged.
-
-    ``removeemptydirs`` controls whether empty directories should be removed.
-
-    ``removefiles`` controls whether files are removed.
-
-    ``abortonerror`` causes an exception to be raised if an error occurs
-    deleting a file or directory.
-
-    ``noop`` controls whether to actually remove files. If not defined, actions
-    will be taken.
-
-    Returns an iterable of relative paths in the working directory that were
-    or would be removed.
-    """
-
-    def remove(removefn, path):
-        try:
-            removefn(path)
-        except OSError:
-            m = _(b'%s cannot be removed') % path
-            if abortonerror:
-                raise error.Abort(m)
-            else:
-                repo.ui.warn(_(b'warning: %s\n') % m)
-
-    # There's no API to copy a matcher. So mutate the passed matcher and
-    # restore it when we're done.
-    oldtraversedir = matcher.traversedir
-
-    res = []
-
-    try:
-        if removeemptydirs:
-            directories = []
-            matcher.traversedir = directories.append
-
-        status = repo.status(match=matcher, ignored=ignored, unknown=unknown)
-
-        if removefiles:
-            for f in sorted(status.unknown + status.ignored):
-                if not noop:
-                    repo.ui.note(_(b'removing file %s\n') % f)
-                    remove(repo.wvfs.unlink, f)
-                res.append(f)
-
-        if removeemptydirs:
-            for f in sorted(directories, reverse=True):
-                if matcher(f) and not repo.wvfs.listdir(f):
-                    if not noop:
-                        repo.ui.note(_(b'removing directory %s\n') % f)
-                        remove(repo.wvfs.rmdir, f)
-                    res.append(f)
-
-        return res
-
-    finally:
-        matcher.traversedir = oldtraversedir
diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -8,21 +8,16 @@
 from __future__ import absolute_import
 
 import errno
-import shutil
 import stat
 import struct
 
 from .i18n import _
 from .node import (
     addednodeid,
-    bin,
-    hex,
     modifiednodeid,
-    nullhex,
     nullid,
     nullrev,
 )
-from .pycompat import delattr
 from .thirdparty import attr
 from . import (
     copies,
@@ -30,6 +25,7 @@
     error,
     filemerge,
     match as matchmod,
+    mergestate as mergestatemod,
     obsutil,
     pathutil,
     pycompat,
@@ -38,741 +34,11 @@
     util,
     worker,
 )
-from .utils import hashutil
 
 _pack = struct.pack
 _unpack = struct.unpack
 
 
-def _droponode(data):
-    # used for compatibility for v1
-    bits = data.split(b'\0')
-    bits = bits[:-2] + bits[-1:]
-    return b'\0'.join(bits)
-
-
-# Merge state record types. See ``mergestate`` docs for more.
-RECORD_LOCAL = b'L'
-RECORD_OTHER = b'O'
-RECORD_MERGED = b'F'
-RECORD_CHANGEDELETE_CONFLICT = b'C'
-RECORD_MERGE_DRIVER_MERGE = b'D'
-RECORD_PATH_CONFLICT = b'P'
-RECORD_MERGE_DRIVER_STATE = b'm'
-RECORD_FILE_VALUES = b'f'
-RECORD_LABELS = b'l'
-RECORD_OVERRIDE = b't'
-RECORD_UNSUPPORTED_MANDATORY = b'X'
-RECORD_UNSUPPORTED_ADVISORY = b'x'
-RECORD_RESOLVED_OTHER = b'R'
-
-MERGE_DRIVER_STATE_UNMARKED = b'u'
-MERGE_DRIVER_STATE_MARKED = b'm'
-MERGE_DRIVER_STATE_SUCCESS = b's'
-
-MERGE_RECORD_UNRESOLVED = b'u'
-MERGE_RECORD_RESOLVED = b'r'
-MERGE_RECORD_UNRESOLVED_PATH = b'pu'
-MERGE_RECORD_RESOLVED_PATH = b'pr'
-MERGE_RECORD_DRIVER_RESOLVED = b'd'
-# represents that the file was automatically merged in favor
-# of other version. This info is used on commit.
-MERGE_RECORD_MERGED_OTHER = b'o'
-
-ACTION_FORGET = b'f'
-ACTION_REMOVE = b'r'
-ACTION_ADD = b'a'
-ACTION_GET = b'g'
-ACTION_PATH_CONFLICT = b'p'
-ACTION_PATH_CONFLICT_RESOLVE = b'pr'
-ACTION_ADD_MODIFIED = b'am'
-ACTION_CREATED = b'c'
-ACTION_DELETED_CHANGED = b'dc'
-ACTION_CHANGED_DELETED = b'cd'
-ACTION_MERGE = b'm'
-ACTION_LOCAL_DIR_RENAME_GET = b'dg'
-ACTION_DIR_RENAME_MOVE_LOCAL = b'dm'
-ACTION_KEEP = b'k'
-ACTION_EXEC = b'e'
-ACTION_CREATED_MERGE = b'cm'
-# GET the other/remote side and store this info in mergestate
-ACTION_GET_OTHER_AND_STORE = b'gs'
-
-
-class mergestate(object):
-    '''track 3-way merge state of individual files
-
-    The merge state is stored on disk when needed. Two files are used: one with
-    an old format (version 1), and one with a new format (version 2). Version 2
-    stores a superset of the data in version 1, including new kinds of records
-    in the future. For more about the new format, see the documentation for
-    `_readrecordsv2`.
-
-    Each record can contain arbitrary content, and has an associated type. This
-    `type` should be a letter. If `type` is uppercase, the record is mandatory:
-    versions of Mercurial that don't support it should abort. If `type` is
-    lowercase, the record can be safely ignored.
-
-    Currently known records:
-
-    L: the node of the "local" part of the merge (hexified version)
-    O: the node of the "other" part of the merge (hexified version)
-    F: a file to be merged entry
-    C: a change/delete or delete/change conflict
-    D: a file that the external merge driver will merge internally
-       (experimental)
-    P: a path conflict (file vs directory)
-    m: the external merge driver defined for this merge plus its run state
-       (experimental)
-    f: a (filename, dictionary) tuple of optional values for a given file
-    X: unsupported mandatory record type (used in tests)
-    x: unsupported advisory record type (used in tests)
-    l: the labels for the parts of the merge.
-
-    Merge driver run states (experimental):
-    u: driver-resolved files unmarked -- needs to be run next time we're about
-       to resolve or commit
-    m: driver-resolved files marked -- only needs to be run before commit
-    s: success/skipped -- does not need to be run any more
-
-    Merge record states (stored in self._state, indexed by filename):
-    u: unresolved conflict
-    r: resolved conflict
-    pu: unresolved path conflict (file conflicts with directory)
-    pr: resolved path conflict
-    d: driver-resolved conflict
-
-    The resolve command transitions between 'u' and 'r' for conflicts and
-    'pu' and 'pr' for path conflicts.
-    '''
-
-    statepathv1 = b'merge/state'
-    statepathv2 = b'merge/state2'
-
-    @staticmethod
-    def clean(repo, node=None, other=None, labels=None):
-        """Initialize a brand new merge state, removing any existing state on
-        disk."""
-        ms = mergestate(repo)
-        ms.reset(node, other, labels)
-        return ms
-
-    @staticmethod
-    def read(repo):
-        """Initialize the merge state, reading it from disk."""
-        ms = mergestate(repo)
-        ms._read()
-        return ms
-
-    def __init__(self, repo):
-        """Initialize the merge state.
-
-        Do not use this directly! Instead call read() or clean()."""
-        self._repo = repo
-        self._dirty = False
-        self._labels = None
-
-    def reset(self, node=None, other=None, labels=None):
-        self._state = {}
-        self._stateextras = {}
-        self._local = None
-        self._other = None
-        self._labels = labels
-        for var in ('localctx', 'otherctx'):
-            if var in vars(self):
-                delattr(self, var)
-        if node:
-            self._local = node
-            self._other = other
-        self._readmergedriver = None
-        if self.mergedriver:
-            self._mdstate = MERGE_DRIVER_STATE_SUCCESS
-        else:
-            self._mdstate = MERGE_DRIVER_STATE_UNMARKED
-        shutil.rmtree(self._repo.vfs.join(b'merge'), True)
-        self._results = {}
-        self._dirty = False
-
-    def _read(self):
-        """Analyse each record content to restore a serialized state from disk
-
-        This function process "record" entry produced by the de-serialization
-        of on disk file.
-        """
-        self._state = {}
-        self._stateextras = {}
-        self._local = None
-        self._other = None
-        for var in ('localctx', 'otherctx'):
-            if var in vars(self):
-                delattr(self, var)
-        self._readmergedriver = None
-        self._mdstate = MERGE_DRIVER_STATE_SUCCESS
-        unsupported = set()
-        records = self._readrecords()
-        for rtype, record in records:
-            if rtype == RECORD_LOCAL:
-                self._local = bin(record)
-            elif rtype == RECORD_OTHER:
-                self._other = bin(record)
-            elif rtype == RECORD_MERGE_DRIVER_STATE:
-                bits = record.split(b'\0', 1)
-                mdstate = bits[1]
-                if len(mdstate) != 1 or mdstate not in (
-                    MERGE_DRIVER_STATE_UNMARKED,
-                    MERGE_DRIVER_STATE_MARKED,
-                    MERGE_DRIVER_STATE_SUCCESS,
-                ):
-                    # the merge driver should be idempotent, so just rerun it
-                    mdstate = MERGE_DRIVER_STATE_UNMARKED
-
-                self._readmergedriver = bits[0]
-                self._mdstate = mdstate
-            elif rtype in (
-                RECORD_MERGED,
-                RECORD_CHANGEDELETE_CONFLICT,
-                RECORD_PATH_CONFLICT,
-                RECORD_MERGE_DRIVER_MERGE,
-                RECORD_RESOLVED_OTHER,
-            ):
-                bits = record.split(b'\0')
-                self._state[bits[0]] = bits[1:]
-            elif rtype == RECORD_FILE_VALUES:
-                filename, rawextras = record.split(b'\0', 1)
-                extraparts = rawextras.split(b'\0')
-                extras = {}
-                i = 0
-                while i < len(extraparts):
-                    extras[extraparts[i]] = extraparts[i + 1]
-                    i += 2
-
-                self._stateextras[filename] = extras
-            elif rtype == RECORD_LABELS:
-                labels = record.split(b'\0', 2)
-                self._labels = [l for l in labels if len(l) > 0]
-            elif not rtype.islower():
-                unsupported.add(rtype)
-        self._results = {}
-        self._dirty = False
-
-        if unsupported:
-            raise error.UnsupportedMergeRecords(unsupported)
-
-    def _readrecords(self):
-        """Read merge state from disk and return a list of record (TYPE, data)
-
-        We read data from both v1 and v2 files and decide which one to use.
-
-        V1 has been used by version prior to 2.9.1 and contains less data than
-        v2. We read both versions and check if no data in v2 contradicts
-        v1. If there is not contradiction we can safely assume that both v1
-        and v2 were written at the same time and use the extract data in v2. If
-        there is contradiction we ignore v2 content as we assume an old version
-        of Mercurial has overwritten the mergestate file and left an old v2
-        file around.
-
-        returns list of record [(TYPE, data), ...]"""
-        v1records = self._readrecordsv1()
-        v2records = self._readrecordsv2()
-        if self._v1v2match(v1records, v2records):
-            return v2records
-        else:
-            # v1 file is newer than v2 file, use it
-            # we have to infer the "other" changeset of the merge
-            # we cannot do better than that with v1 of the format
-            mctx = self._repo[None].parents()[-1]
-            v1records.append((RECORD_OTHER, mctx.hex()))
-            # add place holder "other" file node information
-            # nobody is using it yet so we do no need to fetch the data
-            # if mctx was wrong `mctx[bits[-2]]` may fails.
-            for idx, r in enumerate(v1records):
-                if r[0] == RECORD_MERGED:
-                    bits = r[1].split(b'\0')
-                    bits.insert(-2, b'')
-                    v1records[idx] = (r[0], b'\0'.join(bits))
-            return v1records
-
-    def _v1v2match(self, v1records, v2records):
-        oldv2 = set()  # old format version of v2 record
-        for rec in v2records:
-            if rec[0] == RECORD_LOCAL:
-                oldv2.add(rec)
-            elif rec[0] == RECORD_MERGED:
-                # drop the onode data (not contained in v1)
-                oldv2.add((RECORD_MERGED, _droponode(rec[1])))
-        for rec in v1records:
-            if rec not in oldv2:
-                return False
-        else:
-            return True
-
-    def _readrecordsv1(self):
-        """read on disk merge state for version 1 file
-
-        returns list of record [(TYPE, data), ...]
-
-        Note: the "F" data from this file are one entry short
-              (no "other file node" entry)
-        """
-        records = []
-        try:
-            f = self._repo.vfs(self.statepathv1)
-            for i, l in enumerate(f):
-                if i == 0:
-                    records.append((RECORD_LOCAL, l[:-1]))
-                else:
-                    records.append((RECORD_MERGED, l[:-1]))
-            f.close()
-        except IOError as err:
-            if err.errno != errno.ENOENT:
-                raise
-        return records
-
-    def _readrecordsv2(self):
-        """read on disk merge state for version 2 file
-
-        This format is a list of arbitrary records of the form:
-
-          [type][length][content]
-
-        `type` is a single character, `length` is a 4 byte integer, and
-        `content` is an arbitrary byte sequence of length `length`.
-
-        Mercurial versions prior to 3.7 have a bug where if there are
-        unsupported mandatory merge records, attempting to clear out the merge
-        state with hg update --clean or similar aborts. The 't' record type
-        works around that by writing out what those versions treat as an
-        advisory record, but later versions interpret as special: the first
-        character is the 'real' record type and everything onwards is the data.
-
-        Returns list of records [(TYPE, data), ...]."""
-        records = []
-        try:
-            f = self._repo.vfs(self.statepathv2)
-            data = f.read()
-            off = 0
-            end = len(data)
-            while off < end:
-                rtype = data[off : off + 1]
-                off += 1
-                length = _unpack(b'>I', data[off : (off + 4)])[0]
-                off += 4
-                record = data[off : (off + length)]
-                off += length
-                if rtype == RECORD_OVERRIDE:
-                    rtype, record = record[0:1], record[1:]
-                records.append((rtype, record))
-            f.close()
-        except IOError as err:
-            if err.errno != errno.ENOENT:
-                raise
-        return records
-
-    @util.propertycache
-    def mergedriver(self):
-        # protect against the following:
-        # - A configures a malicious merge driver in their hgrc, then
-        #   pauses the merge
-        # - A edits their hgrc to remove references to the merge driver
-        # - A gives a copy of their entire repo, including .hg, to B
-        # - B inspects .hgrc and finds it to be clean
-        # - B then continues the merge and the malicious merge driver
-        #  gets invoked
-        configmergedriver = self._repo.ui.config(
-            b'experimental', b'mergedriver'
-        )
-        if (
-            self._readmergedriver is not None
-            and self._readmergedriver != configmergedriver
-        ):
-            raise error.ConfigError(
-                _(b"merge driver changed since merge started"),
-                hint=_(b"revert merge driver change or abort merge"),
-            )
-
-        return configmergedriver
-
-    @util.propertycache
-    def local(self):
-        if self._local is None:
-            msg = b"local accessed but self._local isn't set"
-            raise error.ProgrammingError(msg)
-        return self._local
-
-    @util.propertycache
-    def localctx(self):
-        return self._repo[self.local]
-
-    @util.propertycache
-    def other(self):
-        if self._other is None:
-            msg = b"other accessed but self._other isn't set"
-            raise error.ProgrammingError(msg)
-        return self._other
-
-    @util.propertycache
-    def otherctx(self):
-        return self._repo[self.other]
-
-    def active(self):
-        """Whether mergestate is active.
-
-        Returns True if there appears to be mergestate. This is a rough proxy
-        for "is a merge in progress."
-        """
-        return bool(self._local) or bool(self._state)
-
-    def commit(self):
-        """Write current state on disk (if necessary)"""
-        if self._dirty:
-            records = self._makerecords()
-            self._writerecords(records)
-            self._dirty = False
-
-    def _makerecords(self):
-        records = []
-        records.append((RECORD_LOCAL, hex(self._local)))
-        records.append((RECORD_OTHER, hex(self._other)))
-        if self.mergedriver:
-            records.append(
-                (
-                    RECORD_MERGE_DRIVER_STATE,
-                    b'\0'.join([self.mergedriver, self._mdstate]),
-                )
-            )
-        # Write out state items. In all cases, the value of the state map entry
-        # is written as the contents of the record. The record type depends on
-        # the type of state that is stored, and capital-letter records are used
-        # to prevent older versions of Mercurial that do not support the feature
-        # from loading them.
-        for filename, v in pycompat.iteritems(self._state):
-            if v[0] == MERGE_RECORD_DRIVER_RESOLVED:
-                # Driver-resolved merge. These are stored in 'D' records.
-                records.append(
-                    (RECORD_MERGE_DRIVER_MERGE, b'\0'.join([filename] + v))
-                )
-            elif v[0] in (
-                MERGE_RECORD_UNRESOLVED_PATH,
-                MERGE_RECORD_RESOLVED_PATH,
-            ):
-                # Path conflicts. These are stored in 'P' records.  The current
-                # resolution state ('pu' or 'pr') is stored within the record.
-                records.append(
-                    (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v))
-                )
-            elif v[0] == MERGE_RECORD_MERGED_OTHER:
-                records.append(
-                    (RECORD_RESOLVED_OTHER, b'\0'.join([filename] + v))
-                )
-            elif v[1] == nullhex or v[6] == nullhex:
-                # Change/Delete or Delete/Change conflicts. These are stored in
-                # 'C' records. v[1] is the local file, and is nullhex when the
-                # file is deleted locally ('dc'). v[6] is the remote file, and
-                # is nullhex when the file is deleted remotely ('cd').
-                records.append(
-                    (RECORD_CHANGEDELETE_CONFLICT, b'\0'.join([filename] + v))
-                )
-            else:
-                # Normal files.  These are stored in 'F' records.
-                records.append((RECORD_MERGED, b'\0'.join([filename] + v)))
-        for filename, extras in sorted(pycompat.iteritems(self._stateextras)):
-            rawextras = b'\0'.join(
-                b'%s\0%s' % (k, v) for k, v in pycompat.iteritems(extras)
-            )
-            records.append(
-                (RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras))
-            )
-        if self._labels is not None:
-            labels = b'\0'.join(self._labels)
-            records.append((RECORD_LABELS, labels))
-        return records
-
-    def _writerecords(self, records):
-        """Write current state on disk (both v1 and v2)"""
-        self._writerecordsv1(records)
-        self._writerecordsv2(records)
-
-    def _writerecordsv1(self, records):
-        """Write current state on disk in a version 1 file"""
-        f = self._repo.vfs(self.statepathv1, b'wb')
-        irecords = iter(records)
-        lrecords = next(irecords)
-        assert lrecords[0] == RECORD_LOCAL
-        f.write(hex(self._local) + b'\n')
-        for rtype, data in irecords:
-            if rtype == RECORD_MERGED:
-                f.write(b'%s\n' % _droponode(data))
-        f.close()
-
-    def _writerecordsv2(self, records):
-        """Write current state on disk in a version 2 file
-
-        See the docstring for _readrecordsv2 for why we use 't'."""
-        # these are the records that all version 2 clients can read
-        allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
-        f = self._repo.vfs(self.statepathv2, b'wb')
-        for key, data in records:
-            assert len(key) == 1
-            if key not in allowlist:
-                key, data = RECORD_OVERRIDE, b'%s%s' % (key, data)
-            format = b'>sI%is' % len(data)
-            f.write(_pack(format, key, len(data), data))
-        f.close()
-
-    @staticmethod
-    def getlocalkey(path):
-        """hash the path of a local file context for storage in the .hg/merge
-        directory."""
-
-        return hex(hashutil.sha1(path).digest())
-
-    def add(self, fcl, fco, fca, fd):
-        """add a new (potentially?) conflicting file the merge state
-        fcl: file context for local,
-        fco: file context for remote,
-        fca: file context for ancestors,
-        fd:  file path of the resulting merge.
-
-        note: also write the local version to the `.hg/merge` directory.
-        """
-        if fcl.isabsent():
-            localkey = nullhex
-        else:
-            localkey = mergestate.getlocalkey(fcl.path())
-            self._repo.vfs.write(b'merge/' + localkey, fcl.data())
-        self._state[fd] = [
-            MERGE_RECORD_UNRESOLVED,
-            localkey,
-            fcl.path(),
-            fca.path(),
-            hex(fca.filenode()),
-            fco.path(),
-            hex(fco.filenode()),
-            fcl.flags(),
-        ]
-        self._stateextras[fd] = {b'ancestorlinknode': hex(fca.node())}
-        self._dirty = True
-
-    def addpath(self, path, frename, forigin):
-        """add a new conflicting path to the merge state
-        path:    the path that conflicts
-        frename: the filename the conflicting file was renamed to
-        forigin: origin of the file ('l' or 'r' for local/remote)
-        """
-        self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin]
-        self._dirty = True
-
-    def addmergedother(self, path):
-        self._state[path] = [MERGE_RECORD_MERGED_OTHER, nullhex, nullhex]
-        self._dirty = True
-
-    def __contains__(self, dfile):
-        return dfile in self._state
-
-    def __getitem__(self, dfile):
-        return self._state[dfile][0]
-
-    def __iter__(self):
-        return iter(sorted(self._state))
-
-    def files(self):
-        return self._state.keys()
-
-    def mark(self, dfile, state):
-        self._state[dfile][0] = state
-        self._dirty = True
-
-    def mdstate(self):
-        return self._mdstate
-
-    def unresolved(self):
-        """Obtain the paths of unresolved files."""
-
-        for f, entry in pycompat.iteritems(self._state):
-            if entry[0] in (
-                MERGE_RECORD_UNRESOLVED,
-                MERGE_RECORD_UNRESOLVED_PATH,
-            ):
-                yield f
-
-    def driverresolved(self):
-        """Obtain the paths of driver-resolved files."""
-
-        for f, entry in self._state.items():
-            if entry[0] == MERGE_RECORD_DRIVER_RESOLVED:
-                yield f
-
-    def extras(self, filename):
-        return self._stateextras.setdefault(filename, {})
-
-    def _resolve(self, preresolve, dfile, wctx):
-        """rerun merge process for file path `dfile`"""
-        if self[dfile] in (MERGE_RECORD_RESOLVED, MERGE_RECORD_DRIVER_RESOLVED):
-            return True, 0
-        if self._state[dfile][0] == MERGE_RECORD_MERGED_OTHER:
-            return True, 0
-        stateentry = self._state[dfile]
-        state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry
-        octx = self._repo[self._other]
-        extras = self.extras(dfile)
-        anccommitnode = extras.get(b'ancestorlinknode')
-        if anccommitnode:
-            actx = self._repo[anccommitnode]
-        else:
-            actx = None
-        fcd = self._filectxorabsent(localkey, wctx, dfile)
-        fco = self._filectxorabsent(onode, octx, ofile)
-        # TODO: move this to filectxorabsent
-        fca = self._repo.filectx(afile, fileid=anode, changectx=actx)
-        # "premerge" x flags
-        flo = fco.flags()
-        fla = fca.flags()
-        if b'x' in flags + flo + fla and b'l' not in flags + flo + fla:
-            if fca.node() == nullid and flags != flo:
-                if preresolve:
-                    self._repo.ui.warn(
-                        _(
-                            b'warning: cannot merge flags for %s '
-                            b'without common ancestor - keeping local flags\n'
-                        )
-                        % afile
-                    )
-            elif flags == fla:
-                flags = flo
-        if preresolve:
-            # restore local
-            if localkey != nullhex:
-                f = self._repo.vfs(b'merge/' + localkey)
-                wctx[dfile].write(f.read(), flags)
-                f.close()
-            else:
-                wctx[dfile].remove(ignoremissing=True)
-            complete, r, deleted = filemerge.premerge(
-                self._repo,
-                wctx,
-                self._local,
-                lfile,
-                fcd,
-                fco,
-                fca,
-                labels=self._labels,
-            )
-        else:
-            complete, r, deleted = filemerge.filemerge(
-                self._repo,
-                wctx,
-                self._local,
-                lfile,
-                fcd,
-                fco,
-                fca,
-                labels=self._labels,
-            )
-        if r is None:
-            # no real conflict
-            del self._state[dfile]
-            self._stateextras.pop(dfile, None)
-            self._dirty = True
-        elif not r:
-            self.mark(dfile, MERGE_RECORD_RESOLVED)
-
-        if complete:
-            action = None
-            if deleted:
-                if fcd.isabsent():
-                    # dc: local picked. Need to drop if present, which may
-                    # happen on re-resolves.
-                    action = ACTION_FORGET
-                else:
-                    # cd: remote picked (or otherwise deleted)
-                    action = ACTION_REMOVE
-            else:
-                if fcd.isabsent():  # dc: remote picked
-                    action = ACTION_GET
-                elif fco.isabsent():  # cd: local picked
-                    if dfile in self.localctx:
-                        action = ACTION_ADD_MODIFIED
-                    else:
-                        action = ACTION_ADD
-                # else: regular merges (no action necessary)
-            self._results[dfile] = r, action
-
-        return complete, r
-
-    def _filectxorabsent(self, hexnode, ctx, f):
-        if hexnode == nullhex:
-            return filemerge.absentfilectx(ctx, f)
-        else:
-            return ctx[f]
-
-    def preresolve(self, dfile, wctx):
-        """run premerge process for dfile
-
-        Returns whether the merge is complete, and the exit code."""
-        return self._resolve(True, dfile, wctx)
-
-    def resolve(self, dfile, wctx):
-        """run merge process (assuming premerge was run) for dfile
-
-        Returns the exit code of the merge."""
-        return self._resolve(False, dfile, wctx)[1]
-
-    def counts(self):
-        """return counts for updated, merged and removed files in this
-        session"""
-        updated, merged, removed = 0, 0, 0
-        for r, action in pycompat.itervalues(self._results):
-            if r is None:
-                updated += 1
-            elif r == 0:
-                if action == ACTION_REMOVE:
-                    removed += 1
-                else:
-                    merged += 1
-        return updated, merged, removed
-
-    def unresolvedcount(self):
-        """get unresolved count for this merge (persistent)"""
-        return len(list(self.unresolved()))
-
-    def actions(self):
-        """return lists of actions to perform on the dirstate"""
-        actions = {
-            ACTION_REMOVE: [],
-            ACTION_FORGET: [],
-            ACTION_ADD: [],
-            ACTION_ADD_MODIFIED: [],
-            ACTION_GET: [],
-        }
-        for f, (r, action) in pycompat.iteritems(self._results):
-            if action is not None:
-                actions[action].append((f, None, b"merge result"))
-        return actions
-
-    def recordactions(self):
-        """record remove/add/get actions in the dirstate"""
-        branchmerge = self._repo.dirstate.p2() != nullid
-        recordupdates(self._repo, self.actions(), branchmerge, None)
-
-    def queueremove(self, f):
-        """queues a file to be removed from the dirstate
-
-        Meant for use by custom merge drivers."""
-        self._results[f] = 0, ACTION_REMOVE
-
-    def queueadd(self, f):
-        """queues a file to be added to the dirstate
-
-        Meant for use by custom merge drivers."""
-        self._results[f] = 0, ACTION_ADD
-
-    def queueget(self, f):
-        """queues a file to be marked modified in the dirstate
-
-        Meant for use by custom merge drivers."""
-        self._results[f] = 0, ACTION_GET
-
-
 def _getcheckunknownconfig(repo, section, name):
     config = repo.ui.config(section, name)
     valid = [b'abort', b'ignore', b'warn']
@@ -885,14 +151,17 @@
 
         checkunknowndirs = _unknowndirschecker()
         for f, (m, args, msg) in pycompat.iteritems(actions):
-            if m in (ACTION_CREATED, ACTION_DELETED_CHANGED):
+            if m in (
+                mergestatemod.ACTION_CREATED,
+                mergestatemod.ACTION_DELETED_CHANGED,
+            ):
                 if _checkunknownfile(repo, wctx, mctx, f):
                     fileconflicts.add(f)
                 elif pathconfig and f not in wctx:
                     path = checkunknowndirs(repo, wctx, f)
                     if path is not None:
                         pathconflicts.add(path)
-            elif m == ACTION_LOCAL_DIR_RENAME_GET:
+            elif m == mergestatemod.ACTION_LOCAL_DIR_RENAME_GET:
                 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
                     fileconflicts.add(f)
 
@@ -903,7 +172,7 @@
         collectconflicts(unknownconflicts, unknownconfig)
     else:
         for f, (m, args, msg) in pycompat.iteritems(actions):
-            if m == ACTION_CREATED_MERGE:
+            if m == mergestatemod.ACTION_CREATED_MERGE:
                 fl2, anc = args
                 different = _checkunknownfile(repo, wctx, mctx, f)
                 if repo.dirstate._ignore(f):
@@ -924,10 +193,14 @@
                 #     don't like an abort happening in the middle of
                 #     merge.update.
                 if not different:
-                    actions[f] = (ACTION_GET, (fl2, False), b'remote created')
+                    actions[f] = (
+                        mergestatemod.ACTION_GET,
+                        (fl2, False),
+                        b'remote created',
+                    )
                 elif mergeforce or config == b'abort':
                     actions[f] = (
-                        ACTION_MERGE,
+                        mergestatemod.ACTION_MERGE,
                         (f, f, None, False, anc),
                         b'remote differs from untracked local',
                     )
@@ -936,7 +209,11 @@
                 else:
                     if config == b'warn':
                         warnconflicts.add(f)
-                    actions[f] = (ACTION_GET, (fl2, True), b'remote created')
+                    actions[f] = (
+                        mergestatemod.ACTION_GET,
+                        (fl2, True),
+                        b'remote created',
+                    )
 
     for f in sorted(abortconflicts):
         warn = repo.ui.warn
@@ -962,14 +239,14 @@
             repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f)
 
     for f, (m, args, msg) in pycompat.iteritems(actions):
-        if m == ACTION_CREATED:
+        if m == mergestatemod.ACTION_CREATED:
             backup = (
                 f in fileconflicts
                 or f in pathconflicts
                 or any(p in pathconflicts for p in pathutil.finddirs(f))
             )
             (flags,) = args
-            actions[f] = (ACTION_GET, (flags, backup), msg)
+            actions[f] = (mergestatemod.ACTION_GET, (flags, backup), msg)
 
 
 def _forgetremoved(wctx, mctx, branchmerge):
@@ -988,9 +265,9 @@
     """
 
     actions = {}
-    m = ACTION_FORGET
+    m = mergestatemod.ACTION_FORGET
     if branchmerge:
-        m = ACTION_REMOVE
+        m = mergestatemod.ACTION_REMOVE
     for f in wctx.deleted():
         if f not in mctx:
             actions[f] = m, None, b"forget deleted"
@@ -998,7 +275,11 @@
     if not branchmerge:
         for f in wctx.removed():
             if f not in mctx:
-                actions[f] = ACTION_FORGET, None, b"forget removed"
+                actions[f] = (
+                    mergestatemod.ACTION_FORGET,
+                    None,
+                    b"forget removed",
+                )
 
     return actions
 
@@ -1026,24 +307,24 @@
     if actions:
         # KEEP and EXEC are no-op
         for m in (
-            ACTION_ADD,
-            ACTION_ADD_MODIFIED,
-            ACTION_FORGET,
-            ACTION_GET,
-            ACTION_CHANGED_DELETED,
-            ACTION_DELETED_CHANGED,
+            mergestatemod.ACTION_ADD,
+            mergestatemod.ACTION_ADD_MODIFIED,
+            mergestatemod.ACTION_FORGET,
+            mergestatemod.ACTION_GET,
+            mergestatemod.ACTION_CHANGED_DELETED,
+            mergestatemod.ACTION_DELETED_CHANGED,
         ):
             for f, args, msg in actions[m]:
                 pmmf.add(f)
-        for f, args, msg in actions[ACTION_REMOVE]:
+        for f, args, msg in actions[mergestatemod.ACTION_REMOVE]:
             pmmf.discard(f)
-        for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
+        for f, args, msg in actions[mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL]:
             f2, flags = args
             pmmf.discard(f2)
             pmmf.add(f)
-        for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
+        for f, args, msg in actions[mergestatemod.ACTION_LOCAL_DIR_RENAME_GET]:
             pmmf.add(f)
-        for f, args, msg in actions[ACTION_MERGE]:
+        for f, args, msg in actions[mergestatemod.ACTION_MERGE]:
             f1, f2, fa, move, anc = args
             if move:
                 pmmf.discard(f1)
@@ -1128,10 +409,10 @@
 
     for f, (m, args, msg) in actions.items():
         if m in (
-            ACTION_CREATED,
-            ACTION_DELETED_CHANGED,
-            ACTION_MERGE,
-            ACTION_CREATED_MERGE,
+            mergestatemod.ACTION_CREATED,
+            mergestatemod.ACTION_DELETED_CHANGED,
+            mergestatemod.ACTION_MERGE,
+            mergestatemod.ACTION_CREATED_MERGE,
         ):
             # This action may create a new local file.
             createdfiledirs.update(pathutil.finddirs(f))
@@ -1141,13 +422,13 @@
                 # will be checked once we know what all the deleted files are.
                 remoteconflicts.add(f)
         # Track the names of all deleted files.
-        if m == ACTION_REMOVE:
+        if m == mergestatemod.ACTION_REMOVE:
             deletedfiles.add(f)
-        if m == ACTION_MERGE:
+        if m == mergestatemod.ACTION_MERGE:
             f1, f2, fa, move, anc = args
             if move:
                 deletedfiles.add(f1)
-        if m == ACTION_DIR_RENAME_MOVE_LOCAL:
+        if m == mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL:
             f2, flags = args
             deletedfiles.add(f2)
 
@@ -1164,10 +445,10 @@
                 # We will need to rename the local file.
                 localconflicts.add(p)
         if p in actions and actions[p][0] in (
-            ACTION_CREATED,
-            ACTION_DELETED_CHANGED,
-            ACTION_MERGE,
-            ACTION_CREATED_MERGE,
+            mergestatemod.ACTION_CREATED,
+            mergestatemod.ACTION_DELETED_CHANGED,
+            mergestatemod.ACTION_MERGE,
+            mergestatemod.ACTION_CREATED_MERGE,
         ):
             # The file is in a directory which aliases a remote file.
             # This is an internal inconsistency within the remote
@@ -1180,11 +461,15 @@
             ctxname = bytes(wctx).rstrip(b'+')
             pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
             actions[pnew] = (
-                ACTION_PATH_CONFLICT_RESOLVE,
+                mergestatemod.ACTION_PATH_CONFLICT_RESOLVE,
                 (p,),
                 b'local path conflict',
             )
-            actions[p] = (ACTION_PATH_CONFLICT, (pnew, b'l'), b'path conflict')
+            actions[p] = (
+                mergestatemod.ACTION_PATH_CONFLICT,
+                (pnew, b'l'),
+                b'path conflict',
+            )
 
     if remoteconflicts:
         # Check if all files in the conflicting directories have been removed.
@@ -1193,20 +478,23 @@
             if f not in deletedfiles:
                 m, args, msg = actions[p]
                 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
-                if m in (ACTION_DELETED_CHANGED, ACTION_MERGE):
+                if m in (
+                    mergestatemod.ACTION_DELETED_CHANGED,
+                    mergestatemod.ACTION_MERGE,
+                ):
                     # Action was merge, just update target.
                     actions[pnew] = (m, args, msg)
                 else:
                     # Action was create, change to renamed get action.
                     fl = args[0]
                     actions[pnew] = (
-                        ACTION_LOCAL_DIR_RENAME_GET,
+                        mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
                         (p, fl),
                         b'remote path conflict',
                     )
                 actions[p] = (
-                    ACTION_PATH_CONFLICT,
-                    (pnew, ACTION_REMOVE),
+                    mergestatemod.ACTION_PATH_CONFLICT,
+                    (pnew, mergestatemod.ACTION_REMOVE),
                     b'path conflict',
                 )
                 remoteconflicts.remove(p)
@@ -1340,13 +628,13 @@
                 ) or branch_copies2.copy.get(f, None)
                 if fa is not None:
                     actions[f] = (
-                        ACTION_MERGE,
+                        mergestatemod.ACTION_MERGE,
                         (f, f, fa, False, pa.node()),
                         b'both renamed from %s' % fa,
                     )
                 else:
                     actions[f] = (
-                        ACTION_MERGE,
+                        mergestatemod.ACTION_MERGE,
                         (f, f, None, False, pa.node()),
                         b'both created',
                     )
@@ -1355,35 +643,43 @@
                 fla = ma.flags(f)
                 nol = b'l' not in fl1 + fl2 + fla
                 if n2 == a and fl2 == fla:
-                    actions[f] = (ACTION_KEEP, (), b'remote unchanged')
+                    actions[f] = (
+                        mergestatemod.ACTION_KEEP,
+                        (),
+                        b'remote unchanged',
+                    )
                 elif n1 == a and fl1 == fla:  # local unchanged - use remote
                     if n1 == n2:  # optimization: keep local content
                         actions[f] = (
-                            ACTION_EXEC,
+                            mergestatemod.ACTION_EXEC,
                             (fl2,),
                             b'update permissions',
                         )
                     else:
                         actions[f] = (
-                            ACTION_GET_OTHER_AND_STORE
+                            mergestatemod.ACTION_GET_OTHER_AND_STORE
                             if branchmerge
-                            else ACTION_GET,
+                            else mergestatemod.ACTION_GET,
                             (fl2, False),
                             b'remote is newer',
                         )
                 elif nol and n2 == a:  # remote only changed 'x'
-                    actions[f] = (ACTION_EXEC, (fl2,), b'update permissions')
+                    actions[f] = (
+                        mergestatemod.ACTION_EXEC,
+                        (fl2,),
+                        b'update permissions',
+                    )
                 elif nol and n1 == a:  # local only changed 'x'
                     actions[f] = (
-                        ACTION_GET_OTHER_AND_STORE
+                        mergestatemod.ACTION_GET_OTHER_AND_STORE
                         if branchmerge
-                        else ACTION_GET,
+                        else mergestatemod.ACTION_GET,
                         (fl1, False),
                         b'remote is newer',
                     )
                 else:  # both changed something
                     actions[f] = (
-                        ACTION_MERGE,
+                        mergestatemod.ACTION_MERGE,
                         (f, f, f, False, pa.node()),
                         b'versions differ',
                     )
@@ -1396,30 +692,34 @@
                 f2 = branch_copies1.movewithdir[f]
                 if f2 in m2:
                     actions[f2] = (
-                        ACTION_MERGE,
+                        mergestatemod.ACTION_MERGE,
                         (f, f2, None, True, pa.node()),
                         b'remote directory rename, both created',
                     )
                 else:
                     actions[f2] = (
-                        ACTION_DIR_RENAME_MOVE_LOCAL,
+                        mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,
                         (f, fl1),
                         b'remote directory rename - move from %s' % f,
                     )
             elif f in branch_copies1.copy:
                 f2 = branch_copies1.copy[f]
                 actions[f] = (
-                    ACTION_MERGE,
+                    mergestatemod.ACTION_MERGE,
                     (f, f2, f2, False, pa.node()),
                     b'local copied/moved from %s' % f2,
                 )
             elif f in ma:  # clean, a different, no remote
                 if n1 != ma[f]:
                     if acceptremote:
-                        actions[f] = (ACTION_REMOVE, None, b'remote delete')
+                        actions[f] = (
+                            mergestatemod.ACTION_REMOVE,
+                            None,
+                            b'remote delete',
+                        )
                     else:
                         actions[f] = (
-                            ACTION_CHANGED_DELETED,
+                            mergestatemod.ACTION_CHANGED_DELETED,
                             (f, None, f, False, pa.node()),
                             b'prompt changed/deleted',
                         )
@@ -1427,9 +727,17 @@
                     # This extra 'a' is added by working copy manifest to mark
                     # the file as locally added. We should forget it instead of
                     # deleting it.
-                    actions[f] = (ACTION_FORGET, None, b'remote deleted')
+                    actions[f] = (
+                        mergestatemod.ACTION_FORGET,
+                        None,
+                        b'remote deleted',
+                    )
                 else:
-                    actions[f] = (ACTION_REMOVE, None, b'other deleted')
+                    actions[f] = (
+                        mergestatemod.ACTION_REMOVE,
+                        None,
+                        b'other deleted',
+                    )
         elif n2:  # file exists only on remote side
             if f in copied1:
                 pass  # we'll deal with it on m1 side
@@ -1437,13 +745,13 @@
                 f2 = branch_copies2.movewithdir[f]
                 if f2 in m1:
                     actions[f2] = (
-                        ACTION_MERGE,
+                        mergestatemod.ACTION_MERGE,
                         (f2, f, None, False, pa.node()),
                         b'local directory rename, both created',
                     )
                 else:
                     actions[f2] = (
-                        ACTION_LOCAL_DIR_RENAME_GET,
+                        mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
                         (f, fl2),
                         b'local directory rename - get from %s' % f,
                     )
@@ -1451,13 +759,13 @@
                 f2 = branch_copies2.copy[f]
                 if f2 in m2:
                     actions[f] = (
-                        ACTION_MERGE,
+                        mergestatemod.ACTION_MERGE,
                         (f2, f, f2, False, pa.node()),
                         b'remote copied from %s' % f2,
                     )
                 else:
                     actions[f] = (
-                        ACTION_MERGE,
+                        mergestatemod.ACTION_MERGE,
                         (f2, f, f2, True, pa.node()),
                         b'remote moved from %s' % f2,
                     )
@@ -1474,12 +782,20 @@
                 # Checking whether the files are different is expensive, so we
                 # don't do that when we can avoid it.
                 if not force:
-                    actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
+                    actions[f] = (
+                        mergestatemod.ACTION_CREATED,
+                        (fl2,),
+                        b'remote created',
+                    )
                 elif not branchmerge:
-                    actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
+                    actions[f] = (
+                        mergestatemod.ACTION_CREATED,
+                        (fl2,),
+                        b'remote created',
+                    )
                 else:
                     actions[f] = (
-                        ACTION_CREATED_MERGE,
+                        mergestatemod.ACTION_CREATED_MERGE,
                         (fl2, pa.node()),
                         b'remote created, get or merge',
                     )
@@ -1492,16 +808,20 @@
                         break
                 if df is not None and df in m1:
                     actions[df] = (
-                        ACTION_MERGE,
+                        mergestatemod.ACTION_MERGE,
                         (df, f, f, False, pa.node()),
                         b'local directory rename - respect move '
                         b'from %s' % f,
                     )
                 elif acceptremote:
-                    actions[f] = (ACTION_CREATED, (fl2,), b'remote recreating')
+                    actions[f] = (
+                        mergestatemod.ACTION_CREATED,
+                        (fl2,),
+                        b'remote recreating',
+                    )
                 else:
                     actions[f] = (
-                        ACTION_DELETED_CHANGED,
+                        mergestatemod.ACTION_DELETED_CHANGED,
                         (None, f, f, False, pa.node()),
                         b'prompt deleted/changed',
                     )
@@ -1528,14 +848,14 @@
     # actions as we resolve trivial conflicts.
     for f, (m, args, msg) in list(actions.items()):
         if (
-            m == ACTION_CHANGED_DELETED
+            m == mergestatemod.ACTION_CHANGED_DELETED
             and f in ancestor
             and not wctx[f].cmp(ancestor[f])
         ):
             # local did change but ended up with same content
-            actions[f] = ACTION_REMOVE, None, b'prompt same'
+            actions[f] = mergestatemod.ACTION_REMOVE, None, b'prompt same'
         elif (
-            m == ACTION_DELETED_CHANGED
+            m == mergestatemod.ACTION_DELETED_CHANGED
             and f in ancestor
             and not mctx[f].cmp(ancestor[f])
         ):
@@ -1613,8 +933,8 @@
 
             for f, a in sorted(pycompat.iteritems(actions)):
                 m, args, msg = a
-                if m == ACTION_GET_OTHER_AND_STORE:
-                    m = ACTION_GET
+                if m == mergestatemod.ACTION_GET_OTHER_AND_STORE:
+                    m = mergestatemod.ACTION_GET
                 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m))
                 if f in fbids:
                     d = fbids[f]
@@ -1638,14 +958,14 @@
                     actions[f] = l[0]
                     continue
             # If keep is an option, just do it.
-            if ACTION_KEEP in bids:
+            if mergestatemod.ACTION_KEEP in bids:
                 repo.ui.note(_(b" %s: picking 'keep' action\n") % f)
-                actions[f] = bids[ACTION_KEEP][0]
+                actions[f] = bids[mergestatemod.ACTION_KEEP][0]
                 continue
             # If there are gets and they all agree [how could they not?], do it.
-            if ACTION_GET in bids:
-                ga0 = bids[ACTION_GET][0]
-                if all(a == ga0 for a in bids[ACTION_GET][1:]):
+            if mergestatemod.ACTION_GET in bids:
+                ga0 = bids[mergestatemod.ACTION_GET][0]
+                if all(a == ga0 for a in bids[mergestatemod.ACTION_GET][1:]):
                     repo.ui.note(_(b" %s: picking 'get' action\n") % f)
                     actions[f] = ga0
                     continue
@@ -1790,10 +1110,10 @@
     oplist = [
         actions[a]
         for a in (
-            ACTION_GET,
-            ACTION_DELETED_CHANGED,
-            ACTION_LOCAL_DIR_RENAME_GET,
-            ACTION_MERGE,
+            mergestatemod.ACTION_GET,
+            mergestatemod.ACTION_DELETED_CHANGED,
+            mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
+            mergestatemod.ACTION_MERGE,
         )
     ]
     prefetch = scmutil.prefetchfiles
@@ -1826,21 +1146,21 @@
     return {
         m: []
         for m in (
-            ACTION_ADD,
-            ACTION_ADD_MODIFIED,
-            ACTION_FORGET,
-            ACTION_GET,
-            ACTION_CHANGED_DELETED,
-            ACTION_DELETED_CHANGED,
-            ACTION_REMOVE,
-            ACTION_DIR_RENAME_MOVE_LOCAL,
-            ACTION_LOCAL_DIR_RENAME_GET,
-            ACTION_MERGE,
-            ACTION_EXEC,
-            ACTION_KEEP,
-            ACTION_PATH_CONFLICT,
-            ACTION_PATH_CONFLICT_RESOLVE,
-            ACTION_GET_OTHER_AND_STORE,
+            mergestatemod.ACTION_ADD,
+            mergestatemod.ACTION_ADD_MODIFIED,
+            mergestatemod.ACTION_FORGET,
+            mergestatemod.ACTION_GET,
+            mergestatemod.ACTION_CHANGED_DELETED,
+            mergestatemod.ACTION_DELETED_CHANGED,
+            mergestatemod.ACTION_REMOVE,
+            mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL,
+            mergestatemod.ACTION_LOCAL_DIR_RENAME_GET,
+            mergestatemod.ACTION_MERGE,
+            mergestatemod.ACTION_EXEC,
+            mergestatemod.ACTION_KEEP,
+            mergestatemod.ACTION_PATH_CONFLICT,
+            mergestatemod.ACTION_PATH_CONFLICT_RESOLVE,
+            mergestatemod.ACTION_GET_OTHER_AND_STORE,
         )
     }
 
@@ -1862,10 +1182,12 @@
     _prefetchfiles(repo, mctx, actions)
 
     updated, merged, removed = 0, 0, 0
-    ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels)
+    ms = mergestatemod.mergestate.clean(
+        repo, wctx.p1().node(), mctx.node(), labels
+    )
 
     # add ACTION_GET_OTHER_AND_STORE to mergestate
-    for e in actions[ACTION_GET_OTHER_AND_STORE]:
+    for e in actions[mergestatemod.ACTION_GET_OTHER_AND_STORE]:
         ms.addmergedother(e[0])
 
     moves = []
@@ -1873,9 +1195,9 @@
         l.sort()
 
     # 'cd' and 'dc' actions are treated like other merge conflicts
-    mergeactions = sorted(actions[ACTION_CHANGED_DELETED])
-    mergeactions.extend(sorted(actions[ACTION_DELETED_CHANGED]))
-    mergeactions.extend(actions[ACTION_MERGE])
+    mergeactions = sorted(actions[mergestatemod.ACTION_CHANGED_DELETED])
+    mergeactions.extend(sorted(actions[mergestatemod.ACTION_DELETED_CHANGED]))
+    mergeactions.extend(actions[mergestatemod.ACTION_MERGE])
     for f, args, msg in mergeactions:
         f1, f2, fa, move, anc = args
         if f == b'.hgsubstate':  # merged internally
@@ -1906,16 +1228,22 @@
             wctx[f].audit()
             wctx[f].remove()
 
-    numupdates = sum(len(l) for m, l in actions.items() if m != ACTION_KEEP)
+    numupdates = sum(
+        len(l) for m, l in actions.items() if m != mergestatemod.ACTION_KEEP
+    )
     progress = repo.ui.makeprogress(
         _(b'updating'), unit=_(b'files'), total=numupdates
     )
 
-    if [a for a in actions[ACTION_REMOVE] if a[0] == b'.hgsubstate']:
+    if [
+        a
+        for a in actions[mergestatemod.ACTION_REMOVE]
+        if a[0] == b'.hgsubstate'
+    ]:
         subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
 
     # record path conflicts
-    for f, args, msg in actions[ACTION_PATH_CONFLICT]:
+    for f, args, msg in actions[mergestatemod.ACTION_PATH_CONFLICT]:
         f1, fo = args
         s = repo.ui.status
         s(
@@ -1939,14 +1267,18 @@
 
     # remove in parallel (must come before resolving path conflicts and getting)
     prog = worker.worker(
-        repo.ui, cost, batchremove, (repo, wctx), actions[ACTION_REMOVE]
+        repo.ui,
+        cost,
+        batchremove,
+        (repo, wctx),
+        actions[mergestatemod.ACTION_REMOVE],
     )
     for i, item in prog:
         progress.increment(step=i, item=item)
-    removed = len(actions[ACTION_REMOVE])
+    removed = len(actions[mergestatemod.ACTION_REMOVE])
 
     # resolve path conflicts (must come before getting)
-    for f, args, msg in actions[ACTION_PATH_CONFLICT_RESOLVE]:
+    for f, args, msg in actions[mergestatemod.ACTION_PATH_CONFLICT_RESOLVE]:
         repo.ui.debug(b" %s: %s -> pr\n" % (f, msg))
         (f0,) = args
         if wctx[f0].lexists():
@@ -1965,7 +1297,7 @@
         cost,
         batchget,
         (repo, mctx, wctx, wantfiledata),
-        actions[ACTION_GET],
+        actions[mergestatemod.ACTION_GET],
         threadsafe=threadsafe,
         hasretval=True,
     )
@@ -1976,33 +1308,33 @@
         else:
             i, item = res
             progress.increment(step=i, item=item)
-    updated = len(actions[ACTION_GET])
+    updated = len(actions[mergestatemod.ACTION_GET])
 
-    if [a for a in actions[ACTION_GET] if a[0] == b'.hgsubstate']:
+    if [a for a in actions[mergestatemod.ACTION_GET] if a[0] == b'.hgsubstate']:
         subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
 
     # forget (manifest only, just log it) (must come first)
-    for f, args, msg in actions[ACTION_FORGET]:
+    for f, args, msg in actions[mergestatemod.ACTION_FORGET]:
         repo.ui.debug(b" %s: %s -> f\n" % (f, msg))
         progress.increment(item=f)
 
     # re-add (manifest only, just log it)
-    for f, args, msg in actions[ACTION_ADD]:
+    for f, args, msg in actions[mergestatemod.ACTION_ADD]:
         repo.ui.debug(b" %s: %s -> a\n" % (f, msg))
         progress.increment(item=f)
 
     # re-add/mark as modified (manifest only, just log it)
-    for f, args, msg in actions[ACTION_ADD_MODIFIED]:
+    for f, args, msg in actions[mergestatemod.ACTION_ADD_MODIFIED]:
         repo.ui.debug(b" %s: %s -> am\n" % (f, msg))
         progress.increment(item=f)
 
     # keep (noop, just log it)
-    for f, args, msg in actions[ACTION_KEEP]:
+    for f, args, msg in actions[mergestatemod.ACTION_KEEP]:
         repo.ui.debug(b" %s: %s -> k\n" % (f, msg))
         # no progress
 
     # directory rename, move local
-    for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
+    for f, args, msg in actions[mergestatemod.ACTION_DIR_RENAME_MOVE_LOCAL]:
         repo.ui.debug(b" %s: %s -> dm\n" % (f, msg))
         progress.increment(item=f)
         f0, flags = args
@@ -2013,7 +1345,7 @@
         updated += 1
 
     # local directory rename, get
-    for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
+    for f, args, msg in actions[mergestatemod.ACTION_LOCAL_DIR_RENAME_GET]:
         repo.ui.debug(b" %s: %s -> dg\n" % (f, msg))
         progress.increment(item=f)
         f0, flags = args
@@ -2022,7 +1354,7 @@
         updated += 1
 
     # exec
-    for f, args, msg in actions[ACTION_EXEC]:
+    for f, args, msg in actions[mergestatemod.ACTION_EXEC]:
         repo.ui.debug(b" %s: %s -> e\n" % (f, msg))
         progress.increment(item=f)
         (flags,) = args
@@ -2087,7 +1419,7 @@
     if (
         usemergedriver
         and not unresolved
-        and ms.mdstate() != MERGE_DRIVER_STATE_SUCCESS
+        and ms.mdstate() != mergestatemod.MERGE_DRIVER_STATE_SUCCESS
     ):
         if not driverconclude(repo, ms, wctx, labels=labels):
             # XXX setting unresolved to at least 1 is a hack to make sure we
@@ -2103,10 +1435,10 @@
 
     extraactions = ms.actions()
     if extraactions:
-        mfiles = {a[0] for a in actions[ACTION_MERGE]}
+        mfiles = {a[0] for a in actions[mergestatemod.ACTION_MERGE]}
         for k, acts in pycompat.iteritems(extraactions):
             actions[k].extend(acts)
-            if k == ACTION_GET and wantfiledata:
+            if k == mergestatemod.ACTION_GET and wantfiledata:
                 # no filedata until mergestate is updated to provide it
                 for a in acts:
                     getfiledata[a[0]] = None
@@ -2128,112 +1460,17 @@
             # those lists aren't consulted again.
             mfiles.difference_update(a[0] for a in acts)
 
-        actions[ACTION_MERGE] = [
-            a for a in actions[ACTION_MERGE] if a[0] in mfiles
+        actions[mergestatemod.ACTION_MERGE] = [
+            a for a in actions[mergestatemod.ACTION_MERGE] if a[0] in mfiles
         ]
 
     progress.complete()
-    assert len(getfiledata) == (len(actions[ACTION_GET]) if wantfiledata else 0)
+    assert len(getfiledata) == (
+        len(actions[mergestatemod.ACTION_GET]) if wantfiledata else 0
+    )
     return updateresult(updated, merged, removed, unresolved), getfiledata
 
 
-def recordupdates(repo, actions, branchmerge, getfiledata):
-    """record merge actions to the dirstate"""
-    # remove (must come first)
-    for f, args, msg in actions.get(ACTION_REMOVE, []):
-        if branchmerge:
-            repo.dirstate.remove(f)
-        else:
-            repo.dirstate.drop(f)
-
-    # forget (must come first)
-    for f, args, msg in actions.get(ACTION_FORGET, []):
-        repo.dirstate.drop(f)
-
-    # resolve path conflicts
-    for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []):
-        (f0,) = args
-        origf0 = repo.dirstate.copied(f0) or f0
-        repo.dirstate.add(f)
-        repo.dirstate.copy(origf0, f)
-        if f0 == origf0:
-            repo.dirstate.remove(f0)
-        else:
-            repo.dirstate.drop(f0)
-
-    # re-add
-    for f, args, msg in actions.get(ACTION_ADD, []):
-        repo.dirstate.add(f)
-
-    # re-add/mark as modified
-    for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []):
-        if branchmerge:
-            repo.dirstate.normallookup(f)
-        else:
-            repo.dirstate.add(f)
-
-    # exec change
-    for f, args, msg in actions.get(ACTION_EXEC, []):
-        repo.dirstate.normallookup(f)
-
-    # keep
-    for f, args, msg in actions.get(ACTION_KEEP, []):
-        pass
-
-    # get
-    for f, args, msg in actions.get(ACTION_GET, []):
-        if branchmerge:
-            repo.dirstate.otherparent(f)
-        else:
-            parentfiledata = getfiledata[f] if getfiledata else None
-            repo.dirstate.normal(f, parentfiledata=parentfiledata)
-
-    # merge
-    for f, args, msg in actions.get(ACTION_MERGE, []):
-        f1, f2, fa, move, anc = args
-        if branchmerge:
-            # We've done a branch merge, mark this file as merged
-            # so that we properly record the merger later
-            repo.dirstate.merge(f)
-            if f1 != f2:  # copy/rename
-                if move:
-                    repo.dirstate.remove(f1)
-                if f1 != f:
-                    repo.dirstate.copy(f1, f)
-                else:
-                    repo.dirstate.copy(f2, f)
-        else:
-            # We've update-merged a locally modified file, so
-            # we set the dirstate to emulate a normal checkout
-            # of that file some time in the past. Thus our
-            # merge will appear as a normal local file
-            # modification.
-            if f2 == f:  # file not locally copied/moved
-                repo.dirstate.normallookup(f)
-            if move:
-                repo.dirstate.drop(f1)
-
-    # directory rename, move local
-    for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []):
-        f0, flag = args
-        if branchmerge:
-            repo.dirstate.add(f)
-            repo.dirstate.remove(f0)
-            repo.dirstate.copy(f0, f)
-        else:
-            repo.dirstate.normal(f)
-            repo.dirstate.drop(f0)
-
-    # directory rename, get
-    for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []):
-        f0, flag = args
-        if branchmerge:
-            repo.dirstate.add(f)
-            repo.dirstate.copy(f0, f)
-        else:
-            repo.dirstate.normal(f)
-
-
 UPDATECHECK_ABORT = b'abort'  # handled at higher layers
 UPDATECHECK_NONE = b'none'
 UPDATECHECK_LINEAR = b'linear'
@@ -2356,7 +1593,7 @@
         if not overwrite:
             if len(pl) > 1:
                 raise error.Abort(_(b"outstanding uncommitted merge"))
-            ms = mergestate.read(repo)
+            ms = mergestatemod.mergestate.read(repo)
             if list(ms.unresolved()):
                 raise error.Abort(
                     _(b"outstanding merge conflicts"),
@@ -2443,12 +1680,12 @@
         if updatecheck == UPDATECHECK_NO_CONFLICT:
             for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
                 if m not in (
-                    ACTION_GET,
-                    ACTION_KEEP,
-                    ACTION_EXEC,
-                    ACTION_REMOVE,
-                    ACTION_PATH_CONFLICT_RESOLVE,
-                    ACTION_GET_OTHER_AND_STORE,
+                    mergestatemod.ACTION_GET,
+                    mergestatemod.ACTION_KEEP,
+                    mergestatemod.ACTION_EXEC,
+                    mergestatemod.ACTION_REMOVE,
+                    mergestatemod.ACTION_PATH_CONFLICT_RESOLVE,
+                    mergestatemod.ACTION_GET_OTHER_AND_STORE,
                 ):
                     msg = _(b"conflicting changes")
                     hint = _(b"commit or update --clean to discard changes")
@@ -2462,7 +1699,7 @@
             m, args, msg = actionbyfile[f]
             prompts = filemerge.partextras(labels)
             prompts[b'f'] = f
-            if m == ACTION_CHANGED_DELETED:
+            if m == mergestatemod.ACTION_CHANGED_DELETED:
                 if repo.ui.promptchoice(
                     _(
                         b"local%(l)s changed %(f)s which other%(o)s deleted\n"
@@ -2472,16 +1709,24 @@
                     % prompts,
                     0,
                 ):
-                    actionbyfile[f] = (ACTION_REMOVE, None, b'prompt delete')
+                    actionbyfile[f] = (
+                        mergestatemod.ACTION_REMOVE,
+                        None,
+                        b'prompt delete',
+                    )
                 elif f in p1:
                     actionbyfile[f] = (
-                        ACTION_ADD_MODIFIED,
+                        mergestatemod.ACTION_ADD_MODIFIED,
                         None,
                         b'prompt keep',
                     )
                 else:
-                    actionbyfile[f] = (ACTION_ADD, None, b'prompt keep')
-            elif m == ACTION_DELETED_CHANGED:
+                    actionbyfile[f] = (
+                        mergestatemod.ACTION_ADD,
+                        None,
+                        b'prompt keep',
+                    )
+            elif m == mergestatemod.ACTION_DELETED_CHANGED:
                 f1, f2, fa, move, anc = args
                 flags = p2[f2].flags()
                 if (
@@ -2497,7 +1742,7 @@
                     == 0
                 ):
                     actionbyfile[f] = (
-                        ACTION_GET,
+                        mergestatemod.ACTION_GET,
                         (flags, False),
                         b'prompt recreating',
                     )
@@ -2511,9 +1756,9 @@
                 actions[m] = []
             actions[m].append((f, args, msg))
 
-        # ACTION_GET_OTHER_AND_STORE is a ACTION_GET + store in mergestate
-        for e in actions[ACTION_GET_OTHER_AND_STORE]:
-            actions[ACTION_GET].append(e)
+        # ACTION_GET_OTHER_AND_STORE is a mergestatemod.ACTION_GET + store in mergestate
+        for e in actions[mergestatemod.ACTION_GET_OTHER_AND_STORE]:
+            actions[mergestatemod.ACTION_GET].append(e)
 
         if not util.fscasesensitive(repo.path):
             # check collision between files only in p2 for clean update
@@ -2590,7 +1835,7 @@
             fsmonitorwarning
             and not fsmonitorenabled
             and p1.node() == nullid
-            and len(actions[ACTION_GET]) >= fsmonitorthreshold
+            and len(actions[mergestatemod.ACTION_GET]) >= fsmonitorthreshold
             and pycompat.sysplatform.startswith((b'linux', b'darwin'))
         ):
             repo.ui.warn(
@@ -2609,7 +1854,9 @@
         if updatedirstate:
             with repo.dirstate.parentchange():
                 repo.setparents(fp1, fp2)
-                recordupdates(repo, actions, branchmerge, getfiledata)
+                mergestatemod.recordupdates(
+                    repo, actions, branchmerge, getfiledata
+                )
                 # update completed, clear state
                 util.unlink(repo.vfs.join(b'updatestate'))
 
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -44,7 +44,7 @@
     hook,
     lock as lockmod,
     match as matchmod,
-    merge as mergemod,
+    mergestate as mergestatemod,
     mergeutil,
     namespaces,
     narrowspec,
@@ -2466,7 +2466,7 @@
                 ui.status(
                     _(b'working directory now based on revision %d\n') % parents
                 )
-            mergemod.mergestate.clean(self, self[b'.'].node())
+            mergestatemod.mergestate.clean(self, self[b'.'].node())
 
         # TODO: if we know which new heads may result from this rollback, pass
         # them to destroy(), which will prevent the branchhead cache from being
@@ -2865,10 +2865,10 @@
                 fparent2 = nullid
             elif not fparentancestors:
                 # TODO: this whole if-else might be simplified much more
-                ms = mergemod.mergestate.read(self)
+                ms = mergestatemod.mergestate.read(self)
                 if (
                     fname in ms
-                    and ms[fname] == mergemod.MERGE_RECORD_MERGED_OTHER
+                    and ms[fname] == mergestatemod.MERGE_RECORD_MERGED_OTHER
                 ):
                     fparent1, fparent2 = fparent2, nullid
 
@@ -2966,7 +2966,7 @@
                 self, status, text, user, date, extra
             )
 
-            ms = mergemod.mergestate.read(self)
+            ms = mergestatemod.mergestate.read(self)
             mergeutil.checkunresolved(ms)
 
             # internal config: ui.allowemptycommit
diff --git a/mercurial/hg.py b/mercurial/hg.py
--- a/mercurial/hg.py
+++ b/mercurial/hg.py
@@ -33,6 +33,7 @@
     logcmdutil,
     logexchange,
     merge as mergemod,
+    mergestate as mergestatemod,
     narrowspec,
     node,
     phases,
@@ -1164,7 +1165,7 @@
 
 
 def abortmerge(ui, repo):
-    ms = mergemod.mergestate.read(repo)
+    ms = mergestatemod.mergestate.read(repo)
     if ms.active():
         # there were conflicts
         node = ms.localctx.hex()
diff --git a/mercurial/fileset.py b/mercurial/fileset.py
--- a/mercurial/fileset.py
+++ b/mercurial/fileset.py
@@ -16,7 +16,7 @@
     error,
     filesetlang,
     match as matchmod,
-    merge,
+    mergestate as mergestatemod,
     pycompat,
     registrar,
     scmutil,
@@ -245,7 +245,7 @@
     getargs(x, 0, 0, _(b"resolved takes no arguments"))
     if mctx.ctx.rev() is not None:
         return mctx.never()
-    ms = merge.mergestate.read(mctx.ctx.repo())
+    ms = mergestatemod.mergestate.read(mctx.ctx.repo())
     return mctx.predicate(
         lambda f: f in ms and ms[f] == b'r', predrepr=b'resolved'
     )
@@ -259,7 +259,7 @@
     getargs(x, 0, 0, _(b"unresolved takes no arguments"))
     if mctx.ctx.rev() is not None:
         return mctx.never()
-    ms = merge.mergestate.read(mctx.ctx.repo())
+    ms = mergestatemod.mergestate.read(mctx.ctx.repo())
     return mctx.predicate(
         lambda f: f in ms and ms[f] == b'u', predrepr=b'unresolved'
     )
diff --git a/mercurial/debugcommands.py b/mercurial/debugcommands.py
--- a/mercurial/debugcommands.py
+++ b/mercurial/debugcommands.py
@@ -58,7 +58,7 @@
     localrepo,
     lock as lockmod,
     logcmdutil,
-    merge as mergemod,
+    mergestate as mergestatemod,
     obsolete,
     obsutil,
     pathutil,
@@ -1974,7 +1974,7 @@
     was chosen."""
 
     if ui.verbose:
-        ms = mergemod.mergestate(repo)
+        ms = mergestatemod.mergestate(repo)
 
         # sort so that reasonable information is on top
         v1records = ms._readrecordsv1()
@@ -2008,7 +2008,7 @@
             b'"}'
         )
 
-    ms = mergemod.mergestate.read(repo)
+    ms = mergestatemod.mergestate.read(repo)
 
     fm = ui.formatter(b'debugmergestate', opts)
     fm.startitem()
@@ -2034,8 +2034,8 @@
             state = ms._state[f]
             fm_files.data(state=state[0])
             if state[0] in (
-                mergemod.MERGE_RECORD_UNRESOLVED,
-                mergemod.MERGE_RECORD_RESOLVED,
+                mergestatemod.MERGE_RECORD_UNRESOLVED,
+                mergestatemod.MERGE_RECORD_RESOLVED,
             ):
                 fm_files.data(local_key=state[1])
                 fm_files.data(local_path=state[2])
@@ -2045,8 +2045,8 @@
                 fm_files.data(other_node=state[6])
                 fm_files.data(local_flags=state[7])
             elif state[0] in (
-                mergemod.MERGE_RECORD_UNRESOLVED_PATH,
-                mergemod.MERGE_RECORD_RESOLVED_PATH,
+                mergestatemod.MERGE_RECORD_UNRESOLVED_PATH,
+                mergestatemod.MERGE_RECORD_RESOLVED_PATH,
             ):
                 fm_files.data(renamed_path=state[1])
                 fm_files.data(rename_side=state[2])
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -46,6 +46,7 @@
     hg,
     logcmdutil,
     merge as mergemod,
+    mergestate as mergestatemod,
     narrowspec,
     obsolete,
     obsutil,
@@ -5938,7 +5939,7 @@
     if show:
         ui.pager(b'resolve')
         fm = ui.formatter(b'resolve', opts)
-        ms = mergemod.mergestate.read(repo)
+        ms = mergestatemod.mergestate.read(repo)
         wctx = repo[None]
         m = scmutil.match(wctx, pats, opts)
 
@@ -5946,14 +5947,20 @@
         # as 'P'.  Resolved path conflicts show as 'R', the same as normal
         # resolved conflicts.
         mergestateinfo = {
-            mergemod.MERGE_RECORD_UNRESOLVED: (b'resolve.unresolved', b'U'),
-            mergemod.MERGE_RECORD_RESOLVED: (b'resolve.resolved', b'R'),
-            mergemod.MERGE_RECORD_UNRESOLVED_PATH: (
+            mergestatemod.MERGE_RECORD_UNRESOLVED: (
+                b'resolve.unresolved',
+                b'U',
+            ),
+            mergestatemod.MERGE_RECORD_RESOLVED: (b'resolve.resolved', b'R'),
+            mergestatemod.MERGE_RECORD_UNRESOLVED_PATH: (
                 b'resolve.unresolved',
                 b'P',
             ),
-            mergemod.MERGE_RECORD_RESOLVED_PATH: (b'resolve.resolved', b'R'),
-            mergemod.MERGE_RECORD_DRIVER_RESOLVED: (
+            mergestatemod.MERGE_RECORD_RESOLVED_PATH: (
+                b'resolve.resolved',
+                b'R',
+            ),
+            mergestatemod.MERGE_RECORD_DRIVER_RESOLVED: (
                 b'resolve.driverresolved',
                 b'D',
             ),
@@ -5963,7 +5970,7 @@
             if not m(f):
                 continue
 
-            if ms[f] == mergemod.MERGE_RECORD_MERGED_OTHER:
+            if ms[f] == mergestatemod.MERGE_RECORD_MERGED_OTHER:
                 continue
             label, key = mergestateinfo[ms[f]]
             fm.startitem()
@@ -5975,7 +5982,7 @@
         return 0
 
     with repo.wlock():
-        ms = mergemod.mergestate.read(repo)
+        ms = mergestatemod.mergestate.read(repo)
 
         if not (ms.active() or repo.dirstate.p2() != nullid):
             raise error.Abort(
@@ -5986,7 +5993,7 @@
 
         if (
             ms.mergedriver
-            and ms.mdstate() == mergemod.MERGE_DRIVER_STATE_UNMARKED
+            and ms.mdstate() == mergestatemod.MERGE_DRIVER_STATE_UNMARKED
         ):
             proceed = mergemod.driverpreprocess(repo, ms, wctx)
             ms.commit()
@@ -6012,12 +6019,12 @@
 
             didwork = True
 
-            if ms[f] == mergemod.MERGE_RECORD_MERGED_OTHER:
+            if ms[f] == mergestatemod.MERGE_RECORD_MERGED_OTHER:
                 continue
 
             # don't let driver-resolved files be marked, and run the conclude
             # step if asked to resolve
-            if ms[f] == mergemod.MERGE_RECORD_DRIVER_RESOLVED:
+            if ms[f] == mergestatemod.MERGE_RECORD_DRIVER_RESOLVED:
                 exact = m.exact(f)
                 if mark:
                     if exact:
@@ -6037,14 +6044,14 @@
 
             # path conflicts must be resolved manually
             if ms[f] in (
-                mergemod.MERGE_RECORD_UNRESOLVED_PATH,
-                mergemod.MERGE_RECORD_RESOLVED_PATH,
+                mergestatemod.MERGE_RECORD_UNRESOLVED_PATH,
+                mergestatemod.MERGE_RECORD_RESOLVED_PATH,
             ):
                 if mark:
-                    ms.mark(f, mergemod.MERGE_RECORD_RESOLVED_PATH)
+                    ms.mark(f, mergestatemod.MERGE_RECORD_RESOLVED_PATH)
                 elif unmark:
-                    ms.mark(f, mergemod.MERGE_RECORD_UNRESOLVED_PATH)
-                elif ms[f] == mergemod.MERGE_RECORD_UNRESOLVED_PATH:
+                    ms.mark(f, mergestatemod.MERGE_RECORD_UNRESOLVED_PATH)
+                elif ms[f] == mergestatemod.MERGE_RECORD_UNRESOLVED_PATH:
                     ui.warn(
                         _(b'%s: path conflict must be resolved manually\n')
                         % uipathfn(f)
@@ -6056,12 +6063,12 @@
                     fdata = repo.wvfs.tryread(f)
                     if (
                         filemerge.hasconflictmarkers(fdata)
-                        and ms[f] != mergemod.MERGE_RECORD_RESOLVED
+                        and ms[f] != mergestatemod.MERGE_RECORD_RESOLVED
                     ):
                         hasconflictmarkers.append(f)
-                ms.mark(f, mergemod.MERGE_RECORD_RESOLVED)
+                ms.mark(f, mergestatemod.MERGE_RECORD_RESOLVED)
             elif unmark:
-                ms.mark(f, mergemod.MERGE_RECORD_UNRESOLVED)
+                ms.mark(f, mergestatemod.MERGE_RECORD_UNRESOLVED)
             else:
                 # backup pre-resolve (merge uses .orig for its own purposes)
                 a = repo.wjoin(f)
@@ -6942,7 +6949,7 @@
     marks = []
 
     try:
-        ms = mergemod.mergestate.read(repo)
+        ms = mergestatemod.mergestate.read(repo)
     except error.UnsupportedMergeRecords as e:
         s = b' '.join(e.recordtypes)
         ui.warn(
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -38,6 +38,7 @@
     logcmdutil,
     match as matchmod,
     merge as mergemod,
+    mergestate as mergestatemod,
     mergeutil,
     obsolete,
     patch,
@@ -890,7 +891,7 @@
 def readmorestatus(repo):
     """Returns a morestatus object if the repo has unfinished state."""
     statetuple = statemod.getrepostate(repo)
-    mergestate = mergemod.mergestate.read(repo)
+    mergestate = mergestatemod.mergestate.read(repo)
     activemerge = mergestate.active()
     if not statetuple and not activemerge:
         return None
@@ -3127,7 +3128,7 @@
             if subs:
                 subrepoutil.writestate(repo, newsubstate)
 
-        ms = mergemod.mergestate.read(repo)
+        ms = mergestatemod.mergestate.read(repo)
         mergeutil.checkunresolved(ms)
 
         filestoamend = {f for f in wctx.files() if matcher(f)}
diff --git a/hgext/strip.py b/hgext/strip.py
--- a/hgext/strip.py
+++ b/hgext/strip.py
@@ -13,7 +13,7 @@
     error,
     hg,
     lock as lockmod,
-    merge,
+    mergestate as mergestatemod,
     node as nodemod,
     pycompat,
     registrar,
@@ -269,7 +269,7 @@
             repo.dirstate.write(repo.currenttransaction())
 
             # clear resolve state
-            merge.mergestate.clean(repo, repo[b'.'].node())
+            mergestatemod.mergestate.clean(repo, repo[b'.'].node())
 
             update = False
 
diff --git a/hgext/rebase.py b/hgext/rebase.py
--- a/hgext/rebase.py
+++ b/hgext/rebase.py
@@ -36,6 +36,7 @@
     extensions,
     hg,
     merge as mergemod,
+    mergestate as mergestatemod,
     mergeutil,
     node as nodemod,
     obsolete,
@@ -537,7 +538,7 @@
                     user=ctx.user(),
                     date=date,
                 )
-                mergemod.mergestate.clean(repo)
+                mergestatemod.mergestate.clean(repo)
             else:
                 newnode = commitnode(
                     repo,
@@ -1074,7 +1075,7 @@
             )
             # TODO: Make in-memory merge not use the on-disk merge state, so
             # we don't have to clean it here
-            mergemod.mergestate.clean(repo)
+            mergestatemod.mergestate.clean(repo)
             clearstatus(repo)
             clearcollapsemsg(repo)
             return _dorebase(ui, repo, action, opts, inmemory=False)
@@ -1175,7 +1176,7 @@
             if action == b'abort' and opts.get(b'tool', False):
                 ui.warn(_(b'tool option will be ignored\n'))
             if action == b'continue':
-                ms = mergemod.mergestate.read(repo)
+                ms = mergestatemod.mergestate.read(repo)
                 mergeutil.checkunresolved(ms)
 
             retcode = rbsrt._prepareabortorcontinue(
@@ -2185,7 +2186,7 @@
 def continuerebase(ui, repo):
     with repo.wlock(), repo.lock():
         rbsrt = rebaseruntime(repo, ui)
-        ms = mergemod.mergestate.read(repo)
+        ms = mergestatemod.mergestate.read(repo)
         mergeutil.checkunresolved(ms)
         retcode = rbsrt._prepareabortorcontinue(isabort=False)
         if retcode is not None:
diff --git a/hgext/largefiles/overrides.py b/hgext/largefiles/overrides.py
--- a/hgext/largefiles/overrides.py
+++ b/hgext/largefiles/overrides.py
@@ -31,6 +31,7 @@
     logcmdutil,
     match as matchmod,
     merge,
+    mergestate as mergestatemod,
     pathutil,
     pycompat,
     scmutil,
@@ -622,7 +623,7 @@
     return actions, diverge, renamedelete
 
 
- at eh.wrapfunction(merge, b'recordupdates')
+ at eh.wrapfunction(mergestatemod, b'recordupdates')
 def mergerecordupdates(orig, repo, actions, branchmerge, getfiledata):
     if b'lfmr' in actions:
         lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
diff --git a/hgext/histedit.py b/hgext/histedit.py
--- a/hgext/histedit.py
+++ b/hgext/histedit.py
@@ -224,6 +224,7 @@
     hg,
     logcmdutil,
     merge as mergemod,
+    mergestate as mergestatemod,
     mergeutil,
     node,
     obsolete,
@@ -2289,7 +2290,7 @@
 def bootstrapcontinue(ui, state, opts):
     repo = state.repo
 
-    ms = mergemod.mergestate.read(repo)
+    ms = mergestatemod.mergestate.read(repo)
     mergeutil.checkunresolved(ms)
 
     if state.actions:
diff --git a/hgext/fix.py b/hgext/fix.py
--- a/hgext/fix.py
+++ b/hgext/fix.py
@@ -144,6 +144,7 @@
     match as matchmod,
     mdiff,
     merge,
+    mergestate as mergestatemod,
     pycompat,
     registrar,
     rewriteutil,
@@ -426,7 +427,9 @@
     if not (len(revs) == 1 and wdirrev in revs):
         cmdutil.checkunfinished(repo)
         rewriteutil.precheck(repo, revs, b'fix')
-    if wdirrev in revs and list(merge.mergestate.read(repo).unresolved()):
+    if wdirrev in revs and list(
+        mergestatemod.mergestate.read(repo).unresolved()
+    ):
         raise error.Abort(b'unresolved conflicts', hint=b"use 'hg resolve'")
     if not revs:
         raise error.Abort(



To: durin42, martinvonz, #hg-reviewers
Cc: mercurial-patches, mercurial-devel


More information about the Mercurial-patches mailing list