diff --git a/mercurial/mergestate.py b/mercurial/mergestate.py --- a/mercurial/mergestate.py +++ b/mercurial/mergestate.py @@ -92,7 +92,40 @@ } -class mergestate(object): +class _basemergestate(object): + def __init__(self, repo): + self._repo = repo + self._readmergedriver = None + + @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 + + def reset(self, node=None, other=None, labels=None): + self._readmergedriver = None + + +class mergestate(_basemergestate): '''track 3-way merge state of individual files The merge state is stored on disk when needed. Two files are used: one with @@ -161,11 +194,12 @@ """Initialize the merge state. Do not use this directly! Instead call read() or clean().""" - self._repo = repo + super(mergestate, self).__init__(repo) self._dirty = False self._labels = None def reset(self, node=None, other=None, labels=None): + super(mergestate, self).reset(node=node, other=other, labels=labels) self._state = {} self._stateextras = {} self._local = None @@ -177,7 +211,6 @@ if node: self._local = node self._other = other - self._readmergedriver = None if self.mergedriver: self._mdstate = MERGE_DRIVER_STATE_SUCCESS else: @@ -362,30 +395,6 @@ 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" @@ -858,3 +867,113 @@ repo.dirstate.copy(f0, f) else: repo.dirstate.normal(f) + + +class memmergestate(_basemergestate): + def __init__(self, repo, ctx): + super(memmergestate, self).__init__(repo) + self._basectx = ctx + self.reset() + self._ancestor_filectxs = {} + + def add(self, fcl, fco, fca, fd): + """add a new (potentially?) conflicting file to the merge state""" + self._conflicts.add(fcl.path()) + # TODO(augie): the rebase codepath depends on non-implicit + # ancestor. I think we should fix things so that ancestor can + # be passed in to reset(). + # + # Also, you may be tempted to guard this line by + # + # if fca.node() != nullid: + # + # but you'd be misled: this angers some low levels of the + # merge code and it seems the only way to trigger the issue is + # the merge code. + self._ancestor_filectxs[fcl.path()] = fca + + # Since memmergestate isn't mutable yet, these are all trivial + # implementations used by the "happy path" in merge code. + def reset(self, node=None, other=None, labels=None): + super(memmergestate, self).reset(node=node, other=other, labels=labels) + self._local = node + self._other = other + if self._local is not None: + try: + self._ancestor_ctx = next( + self._repo.set(b'ancestor(%ln)', (node, other)) + ) + except StopIteration: + # This will almost certainly be an issue, but if + # callers consistently pass a filectx in add() when we + # can't figure out the ancestor this won't matter. It + # would be a better API for reset() to take an + # ancestor ctx, but that's a broader cleanup. + self._ancestor_ctx = None + self._labels = labels + self._conflicts = set() + + def commit(self): + if self._conflicts: + error.InMemoryMergeConflictsError( + 'cannot commit memmergestate with conflicts, have %d conflicts' + % self.unresolvedcount() + ) + + def counts(self): + return 0, 0, 0 + + def unresolvedcount(self): + return len(self._conflicts) + + def actions(self): + return {} + + @property + def mergedriver(self): + md = super(memmergestate, self).mergedriver + if md: + raise error.InMemoryMergeConflictsError( + b"in-memory merge does not support mergedriver" + ) + return md + + def addmergedother(self, path): + # I'm very dubious this is right. + pass + + def preresolve(self, dfile, wctx): + return self._resolve(True, dfile, wctx) + + def resolve(self, dfile, wctx): + return self._resolve(False, dfile, wctx)[1] + + def _resolve(self, preresolve, dfile, wctx): + # TODO: clean up _filectxorabsent API? + fcd = _filectxorabsent( + nullhex if dfile not in wctx else None, wctx, dfile + ) + octx = self._repo[self._other] + fco = _filectxorabsent( + nullhex if dfile not in octx else None, octx, dfile + ) + actx = self._ancestor_ctx + if dfile in self._ancestor_filectxs: + fca = self._ancestor_filectxs[dfile] + else: + fca = _filectxorabsent( + nullhex if dfile not in actx else None, actx, dfile + ) + fn = filemerge.premerge if preresolve else filemerge.filemerge + complete, mergeret, deleted = fn( + self._repo, + wctx, + self._local, + dfile, # orig + fcd, + fco, + fca, + labels=self._labels, + ) + del deleted # unused + return complete, mergeret