windows: recompute flags when committing a merge (issue1802) stable
authorMatt Mackall <mpm@selenic.com>
Sat, 22 Oct 2011 16:12:33 -0500
branchstable
changeset 15337 cf5f9df6406b
parent 15336 83debcd7064b
child 15338 f4b29792fcda
windows: recompute flags when committing a merge (issue1802) Before this patch, Windows always did the wrong thing with exec bits when committing a merge: consult the flags in first parent. Now we manually recompute the result of merging flags at commit time, which almost always does the right thing (except when there are conflicts between symlink and exec flags). To do this, we: - pull flag synthesis out into its own function - delay building this function unless it's needed - add a merge case that compares flags in local and other against the ancestor This has been tested in multiple ways on Linux: - running the whole test suite with both old and new code in place, checking for differences in each flags() result - running the whole test suite while comparing real on-disk flags against synthetic ones for merges - test-issue1802 (from Martin Geisler) which disables exec bit checking on Unix
mercurial/context.py
mercurial/dirstate.py
tests/test-issue1802.t
--- a/mercurial/context.py	Fri Oct 21 15:09:33 2011 -0500
+++ b/mercurial/context.py	Sat Oct 22 16:12:33 2011 -0500
@@ -632,6 +632,42 @@
     def __contains__(self, key):
         return self._repo.dirstate[key] not in "?r"
 
+    def _buildflagfunc(self):
+        # Create a fallback function for getting file flags when the
+        # filesystem doesn't support them
+
+        copiesget = self._repo.dirstate.copies().get
+
+        if len(self._parents) < 2:
+            # when we have one parent, it's easy: copy from parent
+            man = self._parents[0].manifest()
+            def func(f):
+                f = copiesget(f, f)
+                return man.flags(f)
+        else:
+            # merges are tricky: we try to reconstruct the unstored
+            # result from the merge (issue1802)
+            p1, p2 = self._parents
+            pa = p1.ancestor(p2)
+            m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
+
+            def func(f):
+                f = copiesget(f, f) # may be wrong for merges with copies
+                fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
+                if fl1 == fl2:
+                    return fl1
+                if fl1 == fla:
+                    return fl2
+                if fl2 == fla:
+                    return fl1
+                return '' # punt for conflicts
+
+        return func
+
+    @propertycache
+    def _flagfunc(self):
+        return self._repo.dirstate.flagfunc(self._buildflagfunc)
+
     @propertycache
     def _manifest(self):
         """generate a manifest corresponding to the working directory"""
@@ -640,7 +676,6 @@
             self.status(unknown=True)
 
         man = self._parents[0].manifest().copy()
-        copied = self._repo.dirstate.copies()
         if len(self._parents) > 1:
             man2 = self.p2().manifest()
             def getman(f):
@@ -649,10 +684,9 @@
                 return man2
         else:
             getman = lambda f: man
-        def cf(f):
-            f = copied.get(f, f)
-            return getman(f).flags(f)
-        ff = self._repo.dirstate.flagfunc(cf)
+
+        copied = self._repo.dirstate.copies()
+        ff = self._flagfunc
         modified, added, removed, deleted = self._status
         unknown = self._unknown
         for i, l in (("a", added), ("m", modified), ("u", unknown)):
@@ -767,23 +801,10 @@
             except KeyError:
                 return ''
 
-        orig = self._repo.dirstate.copies().get(path, path)
-
-        def findflag(ctx):
-            mnode = ctx.changeset()[0]
-            node, flag = self._repo.manifest.find(mnode, orig)
-            ff = self._repo.dirstate.flagfunc(lambda x: flag or '')
-            try:
-                return ff(path)
-            except OSError:
-                pass
-
-        flag = findflag(self._parents[0])
-        if flag is None and len(self.parents()) > 1:
-            flag = findflag(self._parents[1])
-        if flag is None or self._repo.dirstate[path] == 'r':
+        try:
+            return self._flagfunc(path)
+        except OSError:
             return ''
-        return flag
 
     def filectx(self, path, filelog=None):
         """get a file context from the working directory"""
--- a/mercurial/dirstate.py	Fri Oct 21 15:09:33 2011 -0500
+++ b/mercurial/dirstate.py	Sat Oct 22 16:12:33 2011 -0500
@@ -131,17 +131,19 @@
         # it's safe because f is always a relative path
         return self._rootdir + f
 
-    def flagfunc(self, fallback):
+    def flagfunc(self, buildfallback):
+        if self._checklink and self._checkexec:
+            def f(x):
+                p = self._join(x)
+                if os.path.islink(p):
+                    return 'l'
+                if util.isexec(p):
+                    return 'x'
+                return ''
+            return f
+
+        fallback = buildfallback()
         if self._checklink:
-            if self._checkexec:
-                def f(x):
-                    p = self._join(x)
-                    if os.path.islink(p):
-                        return 'l'
-                    if util.isexec(p):
-                        return 'x'
-                    return ''
-                return f
             def f(x):
                 if os.path.islink(self._join(x)):
                     return 'l'
@@ -157,7 +159,8 @@
                     return 'x'
                 return ''
             return f
-        return fallback
+        else:
+            return fallback
 
     def getcwd(self):
         cwd = os.getcwd()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-issue1802.t	Sat Oct 22 16:12:33 2011 -0500
@@ -0,0 +1,69 @@
+Create extension that can disable exec checks:
+
+  $ cat > noexec.py <<EOF
+  > from mercurial import extensions, util
+  > def setflags(orig, f, l, x):
+  >     pass
+  > def checkexec(orig, path):
+  >     return False
+  > def extsetup(ui):
+  >     extensions.wrapfunction(util, 'setflags', setflags)
+  >     extensions.wrapfunction(util, 'checkexec', checkexec)
+  > EOF
+
+  $ hg init unix-repo
+  $ cd unix-repo
+  $ touch a
+  $ hg add a
+  $ hg commit -m 'unix: add a'
+  $ hg clone . ../win-repo
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ chmod +x a
+  $ hg commit -m 'unix: chmod a'
+  $ hg manifest -v
+  755 * a
+
+  $ cd ../win-repo
+
+  $ touch b
+  $ hg add b
+  $ hg commit -m 'win: add b'
+
+  $ hg manifest -v
+  644   a
+  644   b
+
+  $ hg pull
+  pulling from $TESTTMP/unix-repo
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 0 changes to 0 files (+1 heads)
+  (run 'hg heads' to see heads, 'hg merge' to merge)
+
+  $ hg manifest -v -r tip
+  755 * a
+
+Simulate a Windows merge:
+
+  $ hg --config extensions.n=$TESTTMP/noexec.py merge --debug
+    searching for copies back to rev 1
+    unmatched files in local:
+     b
+  resolving manifests
+   overwrite None partial False
+   ancestor a03b0deabf2b local d6fa54f68ae1+ remote 2d8bcf2dda39
+   a: update permissions -> e
+  updating: a 1/1 files (100.00%)
+  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+
+Simulate a Windows commit:
+
+  $ hg --config extensions.n=$TESTTMP/noexec.py commit -m 'win: merge'
+
+  $ hg manifest -v
+  755 * a
+  644   b