mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 09:17:30 +03:00
b82acd87f7
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.
201 lines
7.0 KiB
Python
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(),
|
|
}
|