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

Steve Borho steve at borho.org
Thu Oct 14 17:00:23 CDT 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

Ping.

My time window for working on this (aka: vacation) is winding down.

Is this a dead-end?  Should I try another approach?

> 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