[PATCH STABLE] dirstate: ensure mv source is marked deleted when walking icasefs (issue4760)

Matt Harbison mharbison72 at gmail.com
Mon Jul 27 20:29:33 CDT 2015


# HG changeset patch
# User Matt Harbison <matt_harbison at yahoo.com>
# Date 1438046844 14400
#      Mon Jul 27 21:27:24 2015 -0400
# Branch stable
# Node ID 9bb1e828b26d21a40e8b23c83d021701691d2203
# Parent  a74e9806d17d777595f02bef912da25b876cb56f
dirstate: ensure mv source is marked deleted when walking icasefs (issue4760)

Previously, importing a case-only rename patch on a case insensitive filesystem
caused the original file to be marked as '!' in status.  The source was being
forgotten properly in patch.workingbackend.close(), but the call it makes to
scmutil.marktouched() then put the file back into the 'n' state (but it was
still missing from the filesystem).

The cause of this was scmutil._interestingfiles() would walk dirstate, and since
dirstate was able to lstat() the old file via the new name, was treating this as
a forgotten file, not a removed file.  scmutil.marktouched() re-adds forgotten
files, so dirstate got out of sync with the filesystem.

This could be handled with less code in the "kind == regkind or kind == lnkkind"
branch of dirstate._walkexplicit(), but this avoids filesystem accesses unless
case collisions occur.  _discoverpath() is used instead of normalize(), since
the dirstate case is given first precedence, and the old file is still in it.
What matters is the actual case in the filesystem.

diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py
--- a/mercurial/dirstate.py
+++ b/mercurial/dirstate.py
@@ -730,6 +730,34 @@
                     else:
                         badfn(ff, inst.strerror)
 
+        # Case insensitive filesystems cannot rely on lstat() failing to detect
+        # a case-only rename.  Prune the stat object for any file that does not
+        # match the case in the filesystem, if there are multiple files that
+        # normalize to the same path.
+        if match.isexact() and self._checkcase:
+            normed = {}
+
+            for f, st in results.iteritems():
+                if st is None:
+                    continue
+
+                nc = util.normcase(f)
+                paths = normed.get(nc)
+
+                if paths is None:
+                    paths = set()
+                    normed[nc] = paths
+
+                paths.add(f)
+
+            for norm, paths in normed.iteritems():
+                if len(paths) > 1:
+                    for path in paths:
+                        folded = self._discoverpath(path, norm, True, None,
+                                                    self._dirfoldmap)
+                        if path != folded:
+                            results[path] = None
+
         return results, dirsfound, dirsnotfound
 
     def walk(self, match, subrepos, unknown, ignored, full=True):
diff --git a/tests/test-casefolding.t b/tests/test-casefolding.t
--- a/tests/test-casefolding.t
+++ b/tests/test-casefolding.t
@@ -143,6 +143,24 @@
   $ hg update -q -C 3
   $ hg update -q 0
 
+  $ hg up -C -r 2
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg mv A a
+  $ hg diff -g > rename.diff
+  $ hg ci -m 'A -> a'
+  $ hg up -q '.^'
+  $ hg import rename.diff -m "import rename A -> a"
+  applying rename.diff
+  $ hg st
+  ? rename.diff
+  $ hg files
+  a
+  $ find * | sort
+  a
+  rename.diff
+
+  $ rm rename.diff
+
   $ cd ..
 
 issue 3342: file in nested directory causes unexpected abort


More information about the Mercurial-devel mailing list