[PATCH RFC] merge: experimental attempt at removing prompts from merge command

Steve Borho steve at borho.org
Sun Oct 10 14:51:00 CDT 2010


# 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

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)):


More information about the Mercurial-devel mailing list