[Updated] [+------] D8550: mergestate: split out merge state handling code from main merge module
durin42 (Augie Fackler)
phabricator at mercurial-scm.org
Tue May 19 19:56:15 UTC 2020
durin42 updated this revision to Diff 21451.
REPOSITORY
rHG Mercurial
CHANGES SINCE LAST UPDATE
https://phab.mercurial-scm.org/D8550?vs=21427&id=21451
BRANCH
default
CHANGES SINCE LAST ACTION
https://phab.mercurial-scm.org/D8550/new/
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
relnotes/next
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/relnotes/next b/relnotes/next
--- a/relnotes/next
+++ b/relnotes/next
@@ -91,3 +91,6 @@
* The `*others` argument of `cmdutil.check_incompatible_arguments()`
changed from being varargs argument to being a single collection.
+
+ * The `mergestate` class along with some related methods and constants have
+ moved from `mercurial.merge` to a new `mercurial.mergestate` module.
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
More information about the Mercurial-patches
mailing list