# HG changeset patch # User Augie Fackler # Date 1589828399 14400 # Node ID b7808443ed6a6af47f02793ee4ea2a1e52df0e15 # Parent 1d2d353e5c4af6bab0d8e28108a9ece61800e4c2 mergestate: split out merge state handling code from main merge module There's already some pretty reasonable encapsulation here, but I want to make the mergestate storage a property of the context so memctx instances can do a reasonable thing. This is the first step in a reshuffle to make that easier. Differential Revision: https://phab.mercurial-scm.org/D8550 diff -r 1d2d353e5c4a -r b7808443ed6a hgext/fix.py --- a/hgext/fix.py Mon May 18 12:45:45 2020 -0400 +++ b/hgext/fix.py Mon May 18 14:59:59 2020 -0400 @@ -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( diff -r 1d2d353e5c4a -r b7808443ed6a hgext/histedit.py --- a/hgext/histedit.py Mon May 18 12:45:45 2020 -0400 +++ b/hgext/histedit.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a hgext/largefiles/overrides.py --- a/hgext/largefiles/overrides.py Mon May 18 12:45:45 2020 -0400 +++ b/hgext/largefiles/overrides.py Mon May 18 14:59:59 2020 -0400 @@ -31,6 +31,7 @@ logcmdutil, match as matchmod, merge, + mergestate as mergestatemod, pathutil, pycompat, scmutil, @@ -622,7 +623,7 @@ return actions, diverge, renamedelete -@eh.wrapfunction(merge, b'recordupdates') +@eh.wrapfunction(mergestatemod, b'recordupdates') def mergerecordupdates(orig, repo, actions, branchmerge, getfiledata): if b'lfmr' in actions: lfdirstate = lfutil.openlfdirstate(repo.ui, repo) diff -r 1d2d353e5c4a -r b7808443ed6a hgext/rebase.py --- a/hgext/rebase.py Mon May 18 12:45:45 2020 -0400 +++ b/hgext/rebase.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a hgext/strip.py --- a/hgext/strip.py Mon May 18 12:45:45 2020 -0400 +++ b/hgext/strip.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/cmdutil.py --- a/mercurial/cmdutil.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/cmdutil.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/commands.py --- a/mercurial/commands.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/commands.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/debugcommands.py --- a/mercurial/debugcommands.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/debugcommands.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/fileset.py --- a/mercurial/fileset.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/fileset.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/hg.py --- a/mercurial/hg.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/hg.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/localrepo.py --- a/mercurial/localrepo.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/localrepo.py Mon May 18 14:59:59 2020 -0400 @@ -44,7 +44,7 @@ hook, lock as lockmod, match as matchmod, - merge as mergemod, + mergestate as mergestatemod, mergeutil, namespaces, narrowspec, @@ -2468,7 +2468,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 @@ -2867,10 +2867,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 @@ -2968,7 +2968,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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/merge.py --- a/mercurial/merge.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/merge.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/mergestate.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/mergestate.py Mon May 18 14:59:59 2020 -0400 @@ -0,0 +1,850 @@ +from __future__ import absolute_import + +import errno +import shutil +import struct + +from .i18n import _ +from .node import ( + bin, + hex, + nullhex, + nullid, +) +from .pycompat import delattr +from . import ( + error, + filemerge, + pycompat, + util, +) +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 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) diff -r 1d2d353e5c4a -r b7808443ed6a mercurial/narrowspec.py --- a/mercurial/narrowspec.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/narrowspec.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/revset.py --- a/mercurial/revset.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/revset.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/shelve.py --- a/mercurial/shelve.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/shelve.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/sparse.py --- a/mercurial/sparse.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/sparse.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a mercurial/templatekw.py --- a/mercurial/templatekw.py Mon May 18 12:45:45 2020 -0400 +++ b/mercurial/templatekw.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a relnotes/next --- a/relnotes/next Mon May 18 12:45:45 2020 -0400 +++ b/relnotes/next Mon May 18 14:59:59 2020 -0400 @@ -11,3 +11,6 @@ * logcmdutil.diffordiffstat() now takes contexts instead of nodes. + * The `mergestate` class along with some related methods and constants have + moved from `mercurial.merge` to a new `mercurial.mergestate` module. + diff -r 1d2d353e5c4a -r b7808443ed6a tests/fakemergerecord.py --- a/tests/fakemergerecord.py Mon May 18 12:45:45 2020 -0400 +++ b/tests/fakemergerecord.py Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a tests/test-dirstate.t --- a/tests/test-dirstate.t Mon May 18 12:45:45 2020 -0400 +++ b/tests/test-dirstate.t Mon May 18 14:59:59 2020 -0400 @@ -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 -r 1d2d353e5c4a -r b7808443ed6a tests/test-resolve.t --- a/tests/test-resolve.t Mon May 18 12:45:45 2020 -0400 +++ b/tests/test-resolve.t Mon May 18 14:59:59 2020 -0400 @@ -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):