[PATCH 13 of 14] merge: merge file flags together with file content

Mads Kiilerich mads at kiilerich.com
Sun Jan 13 13:02:48 CST 2013


# HG changeset patch
# User Mads Kiilerich <mads at kiilerich.com>
# Date 1357693365 -3600
# Node ID d8ff084807235001d483031c1c2e97a4463e01c7
# Parent  0261266356f25c23224b09de6aebbb2c39c9c242
merge: merge file flags together with file content

The 'x' flag and the 'l' flag are very different. It is usually not a problem
to change the 'x' flag of a normal file independent of the content, but one
does not simply change the type of a file to 'l' independent of the content.

This removes the fmerge function that merged both 'x' and 'l' independent of
content early in the merge process. This correctly introduces some conflicts
instead of silent incorrect merges. 3-way flag merge will now be done in the
resolve process, right next to file content merge. Conflicts can thus be
resolved with (slightly inconvenient) resolve commands like 'resolve f --tool
internal:other'. It thus brings us closer to be able to re-solve manifest merge
after the merge and avoid prompts during merge.

This also removes the "conflicting flags for a - (n)one, e(x)ec or sym(l)ink?"
prompt that nobody could answer and that made it easy to mix symlink targets
and file contents up. Instead it will give a file merge where a sufficiently
clever merge tool can help resolving the issue.

diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -45,11 +45,11 @@ class mergestate(object):
                 f.write("\0".join([d] + v) + "\n")
             f.close()
             self._dirty = False
-    def add(self, fcl, fco, fca, fd, flags):
+    def add(self, fcl, fco, fca, fd):
         hash = util.sha1(fcl.path()).hexdigest()
         self._repo.opener.write("merge/" + hash, fcl.data())
         self._state[fd] = ['u', hash, fcl.path(), fca.path(),
-                           hex(fca.filenode()), fco.path(), flags]
+                           hex(fca.filenode()), fco.path(), fcl.flags()]
         self._dirty = True
     def __contains__(self, dfile):
         return dfile in self._state
@@ -67,12 +67,22 @@ class mergestate(object):
         if self[dfile] == 'r':
             return 0
         state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
+        fcd = wctx[dfile]
+        fco = octx[ofile]
+        fca = self._repo.filectx(afile, fileid=anode)
+        # "premerge" x flags
+        flo = fco.flags()
+        fla = fca.flags()
+        if 'x' in flags + flo + fla and 'l' not in flags + flo + fla:
+            if fca.node() == nullid:
+                self._repo.ui.warn(_('warning: cannot merge flags for %s\n') %
+                                   afile)
+            elif flags == fla:
+                flags = flo
+        # restore local
         f = self._repo.opener("merge/" + hash)
         self._repo.wwrite(dfile, f.read(), flags)
         f.close()
-        fcd = wctx[dfile]
-        fco = octx[ofile]
-        fca = self._repo.filectx(afile, fileid=anode)
         r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
         if r is None:
             # no real conflict
@@ -183,32 +193,6 @@ def manifestmerge(repo, p1, p2, pa, over
     partial = function to filter file lists
     """
 
-    def fmerge(f, f2, fa):
-        """merge flags"""
-        a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
-        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 ""
-        if m and m != a: # changed from a to m
-            return m
-        if n and n != a: # changed from a to n
-            if (n == 'l' or a == 'l') and m1.get(f) != ma.get(f):
-                # can't automatically merge symlink flag when there
-                # are file-level conflicts here, let filemerge take
-                # care of it
-                return m
-            return n
-        return '' # flag was cleared
-
     def act(msg, m, f, *args):
         repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
         actions.append((f, m) + args)
@@ -248,17 +232,25 @@ def manifestmerge(repo, p1, p2, pa, over
         if partial and not partial(f):
             continue
         if f in m2:
-            rflags = fmerge(f, f, f)
+            n2 = m2[f]
+            fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
+            nol = 'l' not in fl1 + fl2 + fla
             a = ma.get(f, nullid)
-            if n == m2[f] or m2[f] == a: # same or local newer
-                # is file locally modified or flags need changing?
-                # dirstate flags may need to be made current
-                if m1.flags(f) != rflags or n[20:]:
-                    act("update permissions", "e", f, rflags)
-            elif n == a: # remote newer
-                act("remote is newer", "g", f, rflags)
-            else: # both changed
-                act("versions differ", "m", f, f, f, rflags, False)
+            if n == n2 and fl1 == fl2:
+                pass # same - keep local
+            elif n2 == a and fl2 == fla:
+                pass # remote unchanged - keep local
+            elif n == a and fl1 == fla: # local unchanged - use remote
+                if n == n2: # optimization: keep local content
+                    act("update permissions", "e", f, fl2)
+                else:
+                    act("remote is newer", "g", f, fl2)
+            elif nol and n2 == a: # remote only changed 'x'
+                act("update permissions", "e", f, fl2)
+            elif nol and n == a: # local only changed 'x'
+                act("remote is newer", "g", f, fl)
+            else: # both changed something
+                act("versions differ", "m", f, f, f, False)
         elif f in copied: # files we'll deal with on m2 side
             pass
         elif f in movewithdir: # directory rename
@@ -267,8 +259,7 @@ def manifestmerge(repo, p1, p2, pa, over
                 m1.flags(f))
         elif f in copy: # case 2 A,B/B/B or case 4,21 A/B/B
             f2 = copy[f]
-            act("local copied/moved to " + f2, "m", f, f2, f,
-                fmerge(f, f2, f2), False)
+            act("local copied/moved to " + f2, "m", f, f2, f, False)
         elif f in ma: # clean, a different, no remote
             if n != ma[f]:
                 if repo.ui.promptchoice(
@@ -296,16 +287,15 @@ def manifestmerge(repo, p1, p2, pa, over
             f2 = copy[f]
             if f2 in m2: # rename case 1, A/A,B/A
                 act("remote copied to " + f, "m",
-                    f2, f, f, fmerge(f2, f, f2), False)
+                    f2, f, f, False)
             else: # case 3,20 A/B/A
                 act("remote moved to " + f, "m",
-                    f2, f, f, fmerge(f2, f, f2), True)
+                    f2, f, f, True)
         elif f not in ma:
             if (not overwrite
                 and _checkunknownfile(repo, p1, p2, f)):
-                rflags = fmerge(f, f, f)
                 act("remote differs from untracked local",
-                    "m", f, f, f, rflags, False)
+                    "m", f, f, f, False)
             else:
                 act("remote created", "g", f, m2.flags(f))
         elif n != ma[f]:
@@ -341,7 +331,7 @@ def applyupdates(repo, actions, wctx, mc
     for a in actions:
         f, m = a[:2]
         if m == "m": # merge
-            f2, fd, flags, move = a[2:]
+            f2, fd, move = a[2:]
             if fd == '.hgsubstate': # merged internally
                 continue
             repo.ui.debug("preserving %s for resolve of %s\n" % (f, fd))
@@ -356,7 +346,7 @@ def applyupdates(repo, actions, wctx, mc
                 fca = fcl.ancestor(fco, actx)
             if not fca:
                 fca = repo.filectx(f, fileid=nullrev)
-            ms.add(fcl, fco, fca, fd, flags)
+            ms.add(fcl, fco, fca, fd)
             if f != fd and move:
                 moves.append(f)
 
@@ -390,7 +380,7 @@ def applyupdates(repo, actions, wctx, mc
                 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
                                  overwrite)
                 continue
-            f2, fd, flags, move = a[2:]
+            f2, fd, move = a[2:]
             audit(fd)
             r = ms.resolve(fd, wctx, mctx)
             if r is not None and r > 0:
@@ -484,7 +474,7 @@ def recordupdates(repo, actions, branchm
             else:
                 repo.dirstate.normal(f)
         elif m == "m": # merge
-            f2, fd, flag, move = a[2:]
+            f2, fd, move = a[2:]
             if branchmerge:
                 # We've done a branch merge, mark this file as merged
                 # so that we properly record the merger later
diff --git a/tests/test-merge-types.t b/tests/test-merge-types.t
--- a/tests/test-merge-types.t
+++ b/tests/test-merge-types.t
@@ -34,16 +34,29 @@ Symlink is local parent, executable is o
   resolving manifests
    overwrite: False, partial: False
    ancestor: c334dc3be0da, local: 521a1e40188f+, remote: 3574f3e69b1c
-   conflicting flags for a
-  (n)one, e(x)ec or sym(l)ink? n
-   a: update permissions -> e
+   a: versions differ -> m
+  preserving a for resolve of a
   updating: a 1/1 files (100.00%)
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
+  picked tool 'internal:merge' for a (binary False symlink True)
+  merging a
+  my a at 521a1e40188f+ other a at 3574f3e69b1c ancestor a at c334dc3be0da
+  warning: internal:merge cannot merge symlinks for a
+  merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
 
   $ tellmeabout a
-  a is a plain file with content:
-  symlink (no-eol)
+  a is a symlink:
+  a -> symlink
+  $ hg resolve a --tool internal:other
+  $ tellmeabout a
+  a is an executable file with content:
+  a
+  $ hg st
+  M a
+  ? a.orig
+  $ rm a.orig
 
 Symlink is other parent, executable is local:
 
@@ -55,17 +68,22 @@ Symlink is other parent, executable is l
   resolving manifests
    overwrite: False, partial: False
    ancestor: c334dc3be0da, local: 3574f3e69b1c+, remote: 521a1e40188f
-   conflicting flags for a
-  (n)one, e(x)ec or sym(l)ink? n
-   a: remote is newer -> g
+   a: versions differ -> m
+  preserving a for resolve of a
   updating: a 1/1 files (100.00%)
-  getting a
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
+  picked tool 'internal:merge' for a (binary False symlink True)
+  merging a
+  my a at 3574f3e69b1c+ other a at 521a1e40188f ancestor a at c334dc3be0da
+  warning: internal:merge cannot merge symlinks for a
+  merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
+  $ rm a.orig
 
   $ tellmeabout a
-  a is a plain file with content:
-  symlink (no-eol)
+  a is an executable file with content:
+  a
 
 Update to link without local change should get us a symlink (issue3316):
 
@@ -119,18 +137,24 @@ where that was what happened.
   $ ln -s base f
   $ hg ci -qm2
   $ hg merge
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
+  merging f
+  warning: internal:merge cannot merge symlinks for f
+  merging f incomplete! (edit conflicts, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
   $ tellmeabout f
   f is a symlink:
-  f -> file
-  content
-  
+  f -> base
 
   $ hg up -Cqr1
   $ hg merge
-  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
+  merging f
+  warning: internal:merge cannot merge symlinks for f
+  merging f incomplete! (edit conflicts, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
   $ tellmeabout f
   f is a plain file with content:
   file
@@ -152,19 +176,27 @@ Test removed 'x' flag merged with change
   $ ln -s dangling f
   $ hg ci -qm2
   $ hg merge
-  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
+  merging f
+  warning: internal:merge cannot merge symlinks for f
+  merging f incomplete! (edit conflicts, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
   $ tellmeabout f
   f is a symlink:
   f -> dangling
 
   $ hg up -Cqr1
   $ hg merge
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
+  merging f
+  warning: internal:merge cannot merge symlinks for f
+  merging f incomplete! (edit conflicts, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+  [1]
   $ tellmeabout f
-  f is a symlink:
-  f -> dangling
+  f is a plain file with content:
+  f
 
   $ cd ..
 
@@ -210,13 +242,10 @@ h: l vs l, different
   $ hg ci -Aqm2
 
   $ hg merge
-   conflicting flags for e
-  (n)one, e(x)ec or sym(l)ink? n
-   conflicting flags for d
-  (n)one, e(x)ec or sym(l)ink? n
   merging a
   warning: conflicts during merge.
   merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
+  warning: cannot merge flags for b
   merging b
   warning: conflicts during merge.
   merging b incomplete! (edit conflicts, then use 'hg resolve --mark')
@@ -229,7 +258,7 @@ h: l vs l, different
   merging h
   warning: internal:merge cannot merge symlinks for h
   merging h incomplete! (edit conflicts, then use 'hg resolve --mark')
-  2 files updated, 0 files merged, 0 files removed, 5 files unresolved
+  3 files updated, 0 files merged, 0 files removed, 5 files unresolved
   use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
   [1]
   $ hg resolve -l
@@ -246,21 +275,21 @@ h: l vs l, different
   1
   >>>>>>> other
   $ tellmeabout b
-  b is an executable file with content:
+  b is a plain file with content:
   <<<<<<< local
   2
   =======
   1
   >>>>>>> other
   $ tellmeabout c
-  c is an executable file with content:
+  c is a plain file with content:
   x
   $ tellmeabout d
-  d is a plain file with content:
-  2 (no-eol)
+  d is a symlink:
+  d -> 2
   $ tellmeabout e
-  e is a plain file with content:
-  x (no-eol)
+  e is a symlink:
+  e -> x
   $ tellmeabout f
   f is a symlink:
   f -> 2
@@ -273,13 +302,10 @@ h: l vs l, different
 
   $ hg up -Cqr1
   $ hg merge
-   conflicting flags for e
-  (n)one, e(x)ec or sym(l)ink? n
-   conflicting flags for d
-  (n)one, e(x)ec or sym(l)ink? n
   merging a
   warning: conflicts during merge.
   merging a incomplete! (edit conflicts, then use 'hg resolve --mark')
+  warning: cannot merge flags for b
   merging b
   warning: conflicts during merge.
   merging b incomplete! (edit conflicts, then use 'hg resolve --mark')
@@ -292,7 +318,7 @@ h: l vs l, different
   merging h
   warning: internal:merge cannot merge symlinks for h
   merging h incomplete! (edit conflicts, then use 'hg resolve --mark')
-  1 files updated, 0 files merged, 0 files removed, 5 files unresolved
+  3 files updated, 0 files merged, 0 files removed, 5 files unresolved
   use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
   [1]
   $ tellmeabout a
@@ -310,13 +336,13 @@ h: l vs l, different
   2
   >>>>>>> other
   $ tellmeabout c
-  c is an executable file with content:
+  c is a plain file with content:
   x
   $ tellmeabout d
-  d is a plain file with content:
+  d is an executable file with content:
   1
   $ tellmeabout e
-  e is a plain file with content:
+  e is an executable file with content:
   x (no-eol)
   $ tellmeabout f
   f is a plain file with content:


More information about the Mercurial-devel mailing list