mirror of
https://github.com/facebook/sapling.git
synced 2024-10-09 08:18:15 +03:00
407941c6f3
Summary: Fixes T25823569. We need to check for mergestate last since some commands leave that file in addition to their own. Reviewed By: DurhamG Differential Revision: D7013846 fbshipit-source-id: 42730bbee81b4c3b3aa2280a48d57da703fab5dd
247 lines
9.1 KiB
Python
247 lines
9.1 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 = [
|
|
['graftstate', {'cmd': 'graft',
|
|
'to_continue': 'graft --continue',
|
|
'to_abort': 'graft --abort'}],
|
|
['rebasestate', {'cmd': 'rebase',
|
|
'to_continue': 'rebase --continue',
|
|
'to_abort': 'rebase --abort'}],
|
|
['shelvedstate', {'cmd': 'unshelve',
|
|
'to_continue': 'unshelve --continue',
|
|
'to_abort': 'unshelve --abort'}],
|
|
['histedit-state', {'cmd': 'histedit',
|
|
'to_continue': 'histedit --continue',
|
|
'to_abort': 'histedit --abort'}],
|
|
# updatestate should be after all other commands, but before mergestate,
|
|
# since some of the above commands run updates which concievably could be
|
|
# interrupted. See coment for mergestate.
|
|
['updatestate', {'cmd': 'update',
|
|
'to_continue': 'update',
|
|
'to_abort': 'update --clean'}],
|
|
# Check for mergestate last, since other commands (shelve, rebase, histedit,
|
|
# etc.) will leave a statefile of their own, as well as a mergestate, if
|
|
# there were conflicts. The command-level statefile should be considered
|
|
# first.
|
|
['merge/state', {'cmd': 'merge',
|
|
'to_continue': 'merge --continue',
|
|
'to_abort': 'update --clean'}],
|
|
]
|
|
|
|
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 path, data in CONFLICTSTATES:
|
|
if repo.vfs.exists(path):
|
|
return data
|
|
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]
|
|
|
|
fileconflicts = []
|
|
pathconflicts = []
|
|
for path in mergestate:
|
|
if not matcher(path):
|
|
continue
|
|
|
|
info = _summarizefileconflicts(mergestate, path, workingctx)
|
|
if info is not None:
|
|
fileconflicts.append(info)
|
|
|
|
info = _summarizepathconflicts(mergestate, path)
|
|
if info is not None:
|
|
pathconflicts.append(info)
|
|
|
|
cmd = _findconflictcommand(repo)
|
|
formatter.startitem()
|
|
formatter.write('conflicts', '%s\n', fileconflicts)
|
|
formatter.write('pathconflicts', '%s\n', pathconflicts)
|
|
formatter.write('command', '%s\n', _findconflictcommand(repo))
|
|
if cmd:
|
|
formatter.write('command', '%s\n', cmd['cmd']) # Deprecated
|
|
formatter.write('command_details', '%s\n', cmd)
|
|
else:
|
|
formatter.write('command', '%s\n', None) # For BC
|
|
formatter.end()
|
|
return 0
|
|
|
|
return orig(ui, repo, *pats, **opts)
|
|
|
|
# To become merge.summarizefileconflicts().
|
|
def _summarizefileconflicts(self, path, workingctx):
|
|
# 'd' = driver-resolved
|
|
# 'r' = marked resolved
|
|
# 'pr', 'pu' = path conflicts
|
|
if self[path] in ('d', 'r', 'pr', 'pu'):
|
|
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 merge.summarizepathconflicts().
|
|
def _summarizepathconflicts(self, path):
|
|
# 'pu' = unresolved path conflict
|
|
if self[path] != 'pu':
|
|
return None
|
|
|
|
stateentry = self._state[path]
|
|
frename = stateentry[1]
|
|
forigin = stateentry[2]
|
|
return {
|
|
'path': path,
|
|
'fileorigin': 'local' if forigin == 'l' else 'remote',
|
|
'renamedto': frename,
|
|
}
|
|
|
|
# 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(),
|
|
}
|