sapling/hgext3rd/conflictinfo.py
Stanislau Hlebik b82acd87f7 conflictinfo: fix test
There was a bug that was introduced upstream in 469914605447 and fixed in
758d59a5f3515b18c767af69f4ed28060f8af56c. This diff reflects the changes.

Also filestat was changed in 4091c920ac07e27ee8536715297127e56d536dab.
2017-06-13 07:15:54 -07:00

201 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.frompath(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(),
}