[PATCH] merge: add resolve --prep, which outputs conflicted comparison files
Durham Goode
durham at fb.com
Thu Feb 23 21:55:38 UTC 2017
Small comments inline for the tests. Overall looks good to me, but
other people should take a look since I had a hand in the design for
this patch and am therefore biased.
On 2/22/17 10:49 PM, Phil Cohen wrote:
> # HG changeset patch
> # User Phil Cohen <phillco at 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
Generally we should make the test repo in a sub directory (hg init
basicprep), so if future additions to this test file want to start a new
repo, they can just cd back to the test root and hg init another repo.
> +
> +2) Can't run prep outside a conflict
> + $ hg resolve --prep -T json
> + abort: no files or directories specified
> + (use --all to re-merge all unresolved files)
> + [255]
> +
> +3) Make a simple conflict
> + $ echo "Unconflicted base, F1" > F1
> + $ echo "Unconflicted base, F2" > F2
> + $ hg add .
> + adding F1
> + adding F2
> + $ hg commit -m "initial commit"
You can use 'hg commit -Aqm "initial commit"' to automatically add files
to the commit. Saves a few lines from the test.
> + $ 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
Do we need to check workingcopy as well?
More information about the Mercurial-devel
mailing list