[PATCH RFC] merge: experimental attempt at removing prompts from merge command
Steve Borho
steve at borho.org
Wed Nov 3 00:43:59 UTC 2010
On Sun, Oct 10, 2010 at 2:51 PM, Steve Borho <steve at borho.org> wrote:
> # HG changeset patch
> # User Steve Borho <steve at borho.org>
> # Date 1286672233 18000
> # Node ID 79ab8c99b5743eb0fdd867c2cf27848d0ac64842
> # Parent c6cdc123f6e4da831ca577044f5b5aefdad929f1
> merge: experimental attempt at removing prompts from merge command
Posting this again to re-open discussion.
One thing I'd like to add to this patch is that the manifest merge and
manifest resolve codes need to be aware if internal:local or
internal:other are specified. In those cases, it can easily pick one
side or the other and skip the prompting.
Any other requests before I begin hacking on this again?
> All interactive prompts are delayed until resolve by recording manifest
> conflicts in the merge status. The idea is that the merge command should
> be non-interactive, only building lists of files that require user resolution.
> The resolve command is intended to be interactive; prompting the user to make
> choices or launching merge tools.
>
> In the parts of manifestmerge that used to prompt the user to resolve
> conflicting file states (deleted vs modified, symlink vs exec) now record
> that a manifest merge is unresolved. This required the introduction of a new
> .hg/merge/mstate file to hold files that are thus pending.
>
> In the two deleted vs modified cases we now default to keeping the modified
> file until the user resolves the manifest conflict. This is a behavior change
> for the "remote changed, local deleted" case, but hopefully an acceptable one.
> For flag conflicts, we keep the local flag until the conflict is resolved.
>
> With this patch, manifest conflicts are not restartable as content conflicts
> are. You cannot mark the file as unresolved and resolve it again. This
> didn't seem a large deal. They were not restartable operations before this patch
> and these are simple choices for the user to make, not three-way text merges.
>
> If the user chooses to accept the deleted side of the manifest conflict, the
> file will show up with missing (!) status instead of removed (R). I've been
> unable to figure out why. After commit, the merge changeset appears to be
> correct.
>
> Lastly, It is likely I've missed some corner conditions involving copies
> happening at the same time as these manifest conflicts. And it's safe to
> assume this will break a number of tests.
>
> diff -r c6cdc123f6e4 -r 79ab8c99b574 mercurial/merge.py
> --- a/mercurial/merge.py Sat Oct 09 15:13:08 2010 -0500
> +++ b/mercurial/merge.py Sat Oct 09 19:57:13 2010 -0500
> @@ -18,12 +18,14 @@
> self._read()
> def reset(self, node=None):
> self._state = {}
> + self._mstate = {}
> if node:
> self._local = node
> shutil.rmtree(self._repo.join("merge"), True)
> self._dirty = False
> def _read(self):
> self._state = {}
> + self._mstate = {}
> try:
> f = self._repo.opener("merge/state")
> for i, l in enumerate(f):
> @@ -32,6 +34,10 @@
> else:
> bits = l[:-1].split("\0")
> self._state[bits[0]] = bits[1:]
> + f = self._repo.opener("merge/mstate")
> + for l in f:
> + bits = l[:-1].split("\0")
> + self._mstate[bits[0]] = bits[1:]
> except IOError, err:
> if err.errno != errno.ENOENT:
> raise
> @@ -42,6 +48,9 @@
> f.write(hex(self._local) + "\n")
> for d, v in self._state.iteritems():
> f.write("\0".join([d] + v) + "\n")
> + f = self._repo.opener("merge/mstate", "w")
> + for d, v in self._mstate.iteritems():
> + f.write("\0".join([d] + v) + "\n")
> self._dirty = False
> def add(self, fcl, fco, fca, fd, flags):
> hash = util.sha1(fcl.path()).hexdigest()
> @@ -49,21 +58,66 @@
> self._state[fd] = ['u', hash, fcl.path(), fca.path(),
> hex(fca.filenode()), fco.path(), flags]
> self._dirty = True
> + def madd(self, fd, op):
> + self._mstate[fd] = ['u', fd, op]
> + self._dirty = True
> def __contains__(self, dfile):
> - return dfile in self._state
> + return dfile in self._state or dfile in self._mstate
> def __getitem__(self, dfile):
> - return self._state[dfile][0]
> + if dfile in self._state:
> + return self._state[dfile][0]
> + return self._mstate[dfile][0]
> def __iter__(self):
> - l = self._state.keys()
> - l.sort()
> - for f in l:
> + l = set(self._state.keys() + self._mstate.keys())
> + for f in sorted(l):
> yield f
> def mark(self, dfile, state):
> - self._state[dfile][0] = state
> + if dfile in self._state:
> + self._state[dfile][0] = state
> + else:
> + self._mstate[dfile][0] = state
> self._dirty = True
> def resolve(self, dfile, wctx, octx):
> if self[dfile] == 'r':
> return 0
> +
> + if dfile in self._mstate:
> + # manifest merge needs user input
> + _s, lfile, op = self._mstate[dfile]
> + repo = self._repo
> + del self._mstate[dfile] # restart not supported
> + if op == "rcld":
> + if repo.ui.promptchoice(
> + " remote changed %s which local deleted\n"
> + "use (c)hanged version or (d)elete?" % dfile,
> + (_("&Changed"), _("&Deleted")), 0):
> + if os.path.lexists(repo.wjoin(dfile)):
> + # TODO: This results in dfile in ! state
> + repo.ui.note("removing %s\n" % dfile)
> + os.unlink(repo.wjoin(dfile))
> + repo.dirstate.forget(dfile)
> + return 0
> + elif op == "lcrd":
> + if repo.ui.promptchoice(
> + _(" local changed %s which remote deleted\n"
> + "use (c)hanged version or (d)elete?") % dfile,
> + (_("&Changed"), _("&Delete")), 0):
> + if os.path.lexists(repo.wjoin(dfile)):
> + # TODO: This results in dfile in ! state
> + repo.ui.note("removing %s\n" % dfile)
> + os.unlink(repo.wjoin(dfile))
> + repo.dirstate.forget(dfile)
> + return 0
> + elif op == "f":
> + r = repo.ui.promptchoice(
> + _(" conflicting flags for %s\n"
> + "(n)one, e(x)ec or sym(l)ink?") % dfile,
> + (_("&None"), _("E&xec"), _("Sym&link")), 0)
> + islink, isexec = ((0,0), (0,1), (1,0))[r]
> + util.set_flags(repo.wjoin(dfile), islink, isexec)
> + if dfile not in self._state:
> + return 0
> +
> state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
> f = self._repo.opener("merge/" + hash)
> self._repo.wwrite(dfile, f.read(), flags)
> @@ -134,15 +188,7 @@
> if m == n: # flags agree
> return m # unchanged
> if m and n and not a: # flags set, don't agree, differ from parent
> - r = repo.ui.promptchoice(
> - _(" conflicting flags for %s\n"
> - "(n)one, e(x)ec or sym(l)ink?") % f,
> - (_("&None"), _("E&xec"), _("Sym&link")), 0)
> - if r == 1:
> - return "x" # Exec
> - if r == 2:
> - return "l" # Symlink
> - return ""
> + act("unresolved flags", "mu", f, "f")
> if m and m != a: # changed from a to m
> return m
> if n and n != a: # changed from a to n
> @@ -207,13 +253,8 @@
> f, f2, f, fmerge(f, f2, f2), False)
> elif f in ma: # clean, a different, no remote
> if n != ma[f]:
> - if repo.ui.promptchoice(
> - _(" local changed %s which remote deleted\n"
> - "use (c)hanged version or (d)elete?") % f,
> - (_("&Changed"), _("&Delete")), 0):
> - act("prompt delete", "r", f)
> - else:
> - act("prompt keep", "a", f)
> + act("temporary keep", "a", f)
> + act("unresolved modify+delete", "mu", f, "lcrd")
> elif n[20:] == "a": # added, no remote
> act("remote deleted", "f", f)
> elif n[20:] != "u":
> @@ -238,11 +279,8 @@
> elif f not in ma:
> act("remote created", "g", f, m2.flags(f))
> elif n != ma[f]:
> - if repo.ui.promptchoice(
> - _("remote changed %s which local deleted\n"
> - "use (c)hanged version or leave (d)eleted?") % f,
> - (_("&Changed"), _("&Deleted")), 0) == 0:
> - act("prompt recreating", "g", f, m2.flags(f))
> + act("temporary recreate", "g", f, m2.flags(f))
> + act("unresolved modify+delete", "mu", f, "rcld")
>
> return action
>
> @@ -288,6 +326,13 @@
> if f != fd and move:
> moves.append(f)
>
> + for a in action:
> + f, m = a[:2]
> + if m == "mu":
> + repo.ui.note(_("%s requires manifest resolve\n") % f)
> + unresolved += 1
> + ms.madd(f, a[2])
> +
> # remove renamed files after safely stored
> for f in moves:
> if os.path.lexists(repo.wjoin(f)):
>
--
Steve Borho
More information about the Mercurial-devel
mailing list