sapling/hgext3rd/conflictinfo.py
Phil Cohen eb2814985f resolve: add internal:dumpjson as an internal merge tool (as an extension, for now)
Summary:
This basically takes the last upstream version of my conflictinfo patch and makes it an extension, so we can get it out to FB/Nuclide users
a bit faster. @asriram has already been developing with this version as a personal extension for a couple of weeks.

I still plan to ship the upstream version through after the freeze ends -- when that happens, we can delete this extension. During the time the two
versions overlap, they shouldn't conflict. (The extension version will still run over the internal version until it's disabled, though.)

Review-wise, this is similar to the last upstream version, except it adds the "command" key that indicates which command generated the conflicts (based on which mutually-exclusive state file exists), and incorporates most of the feedback.

Test Plan: Added a test and ran associated tests.

Reviewers: #mercurial, rmcelroy

Reviewed By: rmcelroy

Subscribers: rmcelroy, asriram, mjpieters

Differential Revision: https://phabricator.intern.facebook.com/D4944709

Signature: t1:4944709:1493148790:a4e798f5bd17ada767ae6c96fe8c8ab973960383
2017-04-25 12:46:28 -07:00

200 lines
7.0 KiB
Python

# conflictinfo.py
#
# Copyright 2017 Facebook, Inc.
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
"""
introduces `--tool=internal:dumpjson` to `resolve` to output conflict info
N.B.: This is an extension-ized version but we hope to land this upstream and
then delete this extension.
Normally, `hg resolve` takes the user on a whistle-stop tour of each conflicted
file, pausing to launch the editor to resolve the conflicts. This is an
alternative workflow for resolving many conflicts in a random-access fashion. It
doesn't change/replace the default behavior.
This commit adds `--tool=internal:dumpjson`. It prints, for each conflict, the
"base", "other", and "ours" versions (the contents, as well as their exec/link
flags), and where the user/tool should write a resolved version (i.e., the
working copy) as JSON. The user will then resolve the conflicts at their leisure
and run `hg resolve --mark`.
"""
from __future__ import absolute_import
import copy
from mercurial.filemerge import absentfilectx
from mercurial.i18n import _
from mercurial import (
commands,
extensions,
merge as mergemod,
scmutil,
)
from mercurial import error
from mercurial import util
testedwith = 'ships-with-fb-hgext'
# `unfinishedstates` would be ideal for this except it does not include merge,
# and doesn't expose the command to run to resume by itself (it instead exposes
# a help string)
# Note: order matters (consider rebase v. merge).
CONFLICTSTATES = [
['graft', 'graftstate'],
['update', 'updatestate'],
['evolve', 'evolvestate'],
['rebase', 'rebasestate'],
['histedit', 'histedit-state'],
['unshelve', 'shelvedstate'],
['merge', 'merge/state'],
]
def extsetup(ui):
extensions.wrapcommand(commands.table, 'resolve', _resolve)
# Returns which command got us into the conflicted merge state. Since these
# states are mutually exclusive, we can use the existence of any one statefile
# as proof of culpability.
def _findconflictcommand(repo):
for name, path in CONFLICTSTATES:
if repo.vfs.exists(path):
return name
return None
# To become a block in commands.py/resolve().
def _resolve(orig, ui, repo, *pats, **opts):
# This block is duplicated from commands.py to maintain behavior.
flaglist = 'all mark unmark list no_status'.split()
all, mark, unmark, show, nostatus = \
[opts.get(o) for o in flaglist]
if (show and (mark or unmark)) or (mark and unmark):
raise error.Abort(_("too many options specified"))
if pats and all:
raise error.Abort(_("can't specify --all and patterns"))
if not (all or pats or show or mark or unmark):
raise error.Abort(_('no files or directories specified'),
hint=('use --all to re-merge all unresolved files'))
# </duplication>
if not show and opts.get('tool', '') == "internal:dumpjson":
formatter = ui.formatter('resolve', {'template': 'json'})
mergestate = mergemod.mergestate.read(repo)
matcher = scmutil.match(repo[None], pats, opts)
workingctx = repo[None]
paths = []
for file in mergestate:
if not matcher(file):
continue
val = _summarizeconflicts(mergestate, file, workingctx)
if val is not None:
paths.append(val)
formatter.startitem()
formatter.write('conflicts', '%s\n', paths)
formatter.write('command', '%s\n', _findconflictcommand(repo))
formatter.end()
return 0
return orig(ui, repo, *pats, **opts)
# To become merge.summarizeconflicts().
def _summarizeconflicts(self, path, workingctx):
# 'd' = driver-resolved
# 'r' = removed
if self[path] in 'rd':
return None
stateentry = self._state[path]
localnode = stateentry[1]
ancestorfile = stateentry[3]
ancestornode = stateentry[4]
otherfile = stateentry[5]
othernode = stateentry[6]
otherctx = self._repo[self._other]
extras = self.extras(path)
anccommitnode = extras.get('ancestorlinknode')
ancestorctx = self._repo[anccommitnode] if anccommitnode else None
workingctx = self._filectxorabsent(localnode, workingctx, path)
otherctx = self._filectxorabsent(othernode, otherctx, otherfile)
basectx = self._repo.filectx(ancestorfile, fileid=ancestornode,
changeid=ancestorctx)
return _summarize(self._repo, workingctx, otherctx, basectx)
# To become filemerge.summarize().
def _summarize(repo, workingfilectx, otherctx, basectx):
origfile = (None if workingfilectx.isabsent() else
scmutil.origpath(repo.ui, repo, repo.wjoin(workingfilectx.path())))
def flags(context):
if isinstance(context, absentfilectx):
return {
'contents': None,
'exists': False,
'isexec': None,
'issymlink': None,
}
return {
'contents': context.data(),
'exists': True,
'isexec': context.isexec(),
'issymlink': context.islink(),
}
output = flags(workingfilectx)
filestat = util.filestat(origfile) if origfile is not None else None
if origfile and filestat.stat:
# Since you can start a merge with a dirty working copy (either via
# `up` or `merge -f`), "local" must reflect that, not the underlying
# changeset. Those contents are available in the .orig version, so we
# look there and mock up the schema to look like the other contexts.
#
# Test cases affected in test-merge-conflict-cornercases.t: #0
local = {
'contents': util.readfile(origfile),
'exists': True,
'isexec': util.isexec(origfile),
'issymlink': util.statislink(filestat.stat),
}
else:
# No backup file. This happens whenever the merge was esoteric enough
# that we didn't launch a merge tool*, and instead prompted the user to
# "use (c)hanged version, (d)elete, or leave (u)nresolved".
#
# The only way to exit that prompt with a conflict is to choose "u",
# which leaves the local version in the working copy (with all its
# pre-merge properties including any local changes), so we can reuse
# that.
#
# Affected test cases: #0b, #1, #6, #11, and #12.
#
# Another alternative might be to use repo['.'][path] but that wouldn't
# have any dirty pre-merge changes.
#
# *If we had, we'd've we would've overwritten the working copy, made a
# backup and hit the above case.
#
# Copy, so the addition of the `path` key below does not affect both
# versions.
local = copy.copy(output)
output['path'] = repo.wjoin(workingfilectx.path())
return {
'base': flags(basectx),
'local': local,
'other': flags(otherctx),
'output': output,
'path': workingfilectx.path(),
}