# HG changeset patch
# User Phil Cohen <phil...@fb.com>
# Date 1487831905 28800
# Wed Feb 22 22:38:25 2017 -0800
# Node ID 77da232f2d689cdb9545e060405e8776f2193488
# Parent 96eaefd350aec869047d9e2da90913ae698463df
merge: add resolve --prep, which outputs conflicted comparison files
Normally when resolving merge conflicts, `hg resolve` loops over each specified
file, generating .orig, ~other.xxxxxx, and ~base.xxxxxx files and then launching
the user's editor (along with a path to the output file with the conflict
markers) and waiting for it to complete.
We'd like to enable random-access to the list of the conflicted files so the
user can flip more easily between conflicted files. This commit introduces
`resolve --prep`, which generates all of these files up-front and outputs the
result as JSON, which could be consumed by an IDE. (We’re not sure if it makes
sense to have a human readable version yet, so we’re leaving that functionality
out until a use case demands it.) The intention is that the user will fix all
the conflicts and then run `resolve --mark`.
Unlike the existing flow, which writes these files to a temporary directory,
these files are stored in `.hg/merge/prep` and get deleted when `.hg/merge`
does. Like the old flow, they have a randomized portion of the filename to
prevent collisions. Technically each call to `resolve --prep` will generate a
new set of files but we consider the cost of this to be low.
No change is made to the existing merge flow as we decided it was not worth
touching the merge state to reuse the same files.
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -33,6 +33,7 @@
error,
exchange,
extensions,
+ formatter,
graphmod,
hbisect,
help,
@@ -4241,7 +4242,8 @@
('l', 'list', None, _('list state of files needing merge')),
('m', 'mark', None, _('mark files as resolved')),
('u', 'unmark', None, _('mark files as unresolved')),
- ('n', 'no-status', None, _('hide status prefix'))]
+ ('n', 'no-status', None, _('hide status prefix')),
+ ('prep', 'prep', None, _('lists paths to comparison file paths'))]
+ mergetoolopts + walkopts + formatteropts,
_('[OPTION]... [FILE]...'),
inferrepo=True)
@@ -4287,8 +4289,8 @@
Returns 0 on success, 1 if any files fail a resolve attempt.
"""
- flaglist = 'all mark unmark list no_status'.split()
- all, mark, unmark, show, nostatus = \
+ flaglist = 'all mark unmark list no_status prep'.split()
+ all, mark, unmark, show, nostatus, prep = \
[opts.get(o) for o in flaglist]
if (show and (mark or unmark)) or (mark and unmark):
@@ -4315,6 +4317,28 @@
fm.end()
return 0
+ if prep:
+ fm = ui.formatter('resolve', opts)
+ if not isinstance(fm, formatter.jsonformatter):
+ raise error.Abort(_('--prep requires `-T json`'))
+ ms = mergemod.mergestate.read(repo)
+ m = scmutil.match(repo[None], pats, opts)
+ wctx = repo[None]
+
+ paths = {}
+ for f in ms:
+ if not m(f):
+ continue
+
+ val = ms.prep(f, wctx)
+ if val is not None:
+ paths[f] = val
+
+ fm.startitem()
+ fm.write('conflicts', '%s\n', paths)
+ fm.end()
+ return 0
+
with repo.wlock():
ms = mergemod.mergestate.read(repo)
diff --git a/mercurial/filemerge.py b/mercurial/filemerge.py
--- a/mercurial/filemerge.py
+++ b/mercurial/filemerge.py
@@ -567,6 +567,35 @@
"o": " [%s]" % labels[1],
}
+# If repo_dir is None, a temp dir is used
+def temp(prefix, ctx, repo, repo_dir=None):
+ fullbase, ext = os.path.splitext(ctx.path())
+ pre = "%s~%s." % (os.path.basename(fullbase), prefix)
+ data = repo.wwritedata(ctx.path(), ctx.data())
+
+ if repo_dir:
+ repo.vfs.makedirs(repo_dir)
+ (fd, name) = repo.vfs.mkstemp(prefix=pre, suffix=ext, dir=repo_dir)
+ f = repo.vfs(name, pycompat.sysstr("wb"))
+ else:
+ (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
+ f = os.fdopen(fd, pycompat.sysstr("wb"))
+
+ f.write(data)
+ f.close()
+ return repo.vfs.join(name)
+
+def gentempfiles(repo, fcd, fco, fca):
+ back = None if fcd.isabsent() else \
+ scmutil.origpath(repo.ui, repo, repo.wjoin(fcd.path()))
+
+ return {
+ 'workingcopy': repo.wjoin(fcd.path()),
+ 'base': temp("base", fca, repo, "merge/prep"),
+ 'other': temp("other", fco, repo, "merge/prep"),
+ 'original': back
+ }
+
def _filemerge(premerge, repo, mynode, orig, fcd, fco, fca, labels=None):
"""perform a 3-way merge in the working directory
@@ -580,16 +609,6 @@
Returns whether the merge is complete, the return value of the merge, and
a boolean indicating whether the file was deleted from disk."""
- def temp(prefix, ctx):
- fullbase, ext = os.path.splitext(ctx.path())
- pre = "%s~%s." % (os.path.basename(fullbase), prefix)
- (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
- data = repo.wwritedata(ctx.path(), ctx.data())
- f = os.fdopen(fd, pycompat.sysstr("wb"))
- f.write(data)
- f.close()
- return name
-
if not fco.cmp(fcd): # files identical?
return True, None, False
@@ -637,8 +656,8 @@
return True, 1, False
a = repo.wjoin(fd)
- b = temp("base", fca)
- c = temp("other", fco)
+ b = temp("base", fca, repo)
+ c = temp("other", fco, repo)
if not fcd.isabsent():
back = scmutil.origpath(ui, repo, a)
if premerge:
diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -456,6 +456,24 @@
def extras(self, filename):
return self._stateextras.setdefault(filename, {})
+ def _prep(self, dfile, wctx):
+ if self[dfile] in 'rd':
+ return None
+ stateentry = self._state[dfile]
+ state, hash, lfile, afile, anode, ofile, onode, flags = stateentry
+ octx = self._repo[self._other]
+ extras = self.extras(dfile)
+ anccommitnode = extras.get('ancestorlinknode')
+ if anccommitnode:
+ actx = self._repo[anccommitnode]
+ else:
+ actx = None
+ fcd = self._filectxorabsent(hash, wctx, dfile)
+ fco = self._filectxorabsent(onode, octx, ofile)
+ fca = self._repo.filectx(afile, fileid=anode, changeid=actx)
+
+ return filemerge.gentempfiles(self._repo, fcd, fco, fca)
+
def _resolve(self, preresolve, dfile, wctx):
"""rerun merge process for file path `dfile`"""
if self[dfile] in 'rd':
@@ -543,6 +561,9 @@
Returns whether the merge is complete, and the exit code."""
return self._resolve(True, dfile, wctx)
+ def prep(self, dfile, wctx):
+ return self._prep(dfile, wctx)
+
def resolve(self, dfile, wctx):
"""run merge process (assuming premerge was run) for dfile
diff --git a/tests/test-resolve-prep.t b/tests/test-resolve-prep.t
new file mode 100644
--- /dev/null
+++ b/tests/test-resolve-prep.t
@@ -0,0 +1,83 @@
+1) Make the repo
+ $ hg init
+ $ echo "First conflicted version, F1" > F1
+ $ echo "First conflicted version, F2" > F2
+ $ hg commit -m "first version, a"
+ $ hg bookmark a
+ $ hg checkout .~1
+ 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (leaving bookmark a)
+ $ echo "Second conflicted version, F1" > F1
+ $ echo "Second conflicted version, F2" > F2
+ $ hg commit -m "second version, b"
+ created new head
+ $ hg bookmark b
+ $ hg log -G -T '({rev}) {desc}\nbookmark: {bookmarks}\nfiles: {files}\n\n'
+ @ (2) second version, b
+ | bookmark: b
+ | files: F1 F2
+ |
+ | o (1) first version, a
+ |/ bookmark: a
+ | files: F1 F2
+ |
+ o (0) initial commit
+ bookmark:
+ files: F1 F2
+
+
+
+ $ hg merge a
+ merging F1
+ merging F2
+ warning: conflicts while merging F1! (edit, then use 'hg resolve --mark')
+ warning: conflicts while merging F2! (edit, then use 'hg resolve --mark')
+ 0 files updated, 0 files merged, 0 files removed, 2 files unresolved
+ use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to
abandon
+ [1]
+
+4) Must pass '-T json':
+ $ hg resolve --prep --all
+ abort: --prep requires `-T json`
+ [255]
+
+5) Get the paths:
+ $ hg resolve --prep --all -T json
+ [
+ {
+ "conflicts": {"F1": {"base": "$TESTTMP/.hg/merge/prep/F1~base.??????", "original": "$TESTTMP/F1.orig", "other": "$TESTTMP/.hg/merge/prep/F1~other.??????", "workingcopy":
"$TESTTMP/F1"}, "F2": {"base": "$TESTTMP/.hg/merge/prep/F2~base.??????", "original": "$TESTTMP/F2.orig", "other": "$TESTTMP/.hg/merge/prep/F2~other.??????", "workingcopy":
"$TESTTMP/F2"}} (glob)
+ }
+ ]
+
+6) Ensure the paths point to the right contents:
+ $ getpath() { # Usage: getpath <path> <version>
+ > local script="import sys, json; print
json.load(sys.stdin)[0][\"conflicts\"][\"$1\"][\"$2\"]"
+ > local result=$(hg resolve --prep --all -T json | python -c "$script")
+ > echo "$result"
+ > }
+ $ cat $(getpath "F1" "base")
+ Unconflicted base, F1
+ $ cat $(getpath "F1" "other")
+ First conflicted version, F1
+ $ cat $(getpath "F1" "original")
+ Second conflicted version, F1
+ $ cat $(getpath "F2" "base")
+ Unconflicted base, F2
+ $ cat $(getpath "F2" "other")
+ First conflicted version, F2
+ $ cat $(getpath "F2" "original")
+ Second conflicted version, F2