mirror of
https://github.com/facebook/sapling.git
synced 2024-10-16 11:52:02 +03:00
463cc8f299
Summary: Update most locations in the hg extensions to use `repo.localvfs` instead of `repo.vfs`. Reviewed By: quark-zju Differential Revision: D9699153 fbshipit-source-id: 48d5f9678caa4961063db30477d6fbe0d6f34347
276 lines
9.1 KiB
Python
276 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 import commands, error, extensions, merge as mergemod, scmutil, util
|
|
from mercurial.filemerge import absentfilectx
|
|
from mercurial.i18n import _
|
|
|
|
|
|
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.localvfs.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(),
|
|
}
|