[PATCH] dirstate: preserve path components case on renames (issue3402)

Patrick Mezard patrick at mezard.eu
Sat Apr 28 13:38:51 CDT 2012


# HG changeset patch
# User Patrick Mezard <patrick at mezard.eu>
# Date 1335637761 -7200
# Branch stable
# Node ID 56ac4b73ab5d3b75751249c88b8055be61da9dc7
# Parent  39d1f83eb05d424fc467d2e0eece8c4a8cefd35a
dirstate: preserve path components case on renames (issue3402)

The original issue was something like:

  $ hg init repo
  $ cd repo
  $ mkdir D
  $ echo a > D/a
  $ hg ci -Am adda
  adding D/a
  $ mv D temp
  $ mv temp d
  $ echo b > d/b
  $ hg add d/b
  adding D/b
  $ hg ci -m addb
  $ hg mv d/b d/c
  moving D/b to d/c
  $ hg st
  A d/c
  R D/b

Here we expected:

  A D/c
  R D/b

the logic being we try to preserve case of path components already known in the
dirstate. This is fixed by the current patch.

Note the following stories are not still not supported:

Changing directory case
  $ hg mv D d
  moving D/a to D/D/a
  moving D/b to D/D/b
  $ hg st
  A D/D/a
  A D/D/b
  R D/a
  R D/b

or:

  $ hg mv D/* d
  D/a: not overwriting - file exists
  D/b: not overwriting - file exists

And if they were, there are probably similar issues with diffing/patching.

diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -268,6 +268,11 @@
     # otarget: ossep
     def copyfile(abssrc, relsrc, otarget, exact):
         abstarget = scmutil.canonpath(repo.root, cwd, otarget)
+        if '/' in abstarget:
+            # We cannot normalize abstarget itself, this would prevent
+            # case only renames, like a => A.
+            abspath, absname = abstarget.rsplit('/', 1)
+            abstarget = repo.dirstate.normalize(abspath) + '/' + absname
         reltarget = repo.pathto(abstarget, cwd)
         target = repo.wjoin(abstarget)
         src = repo.wjoin(abssrc)
diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py
--- a/mercurial/dirstate.py
+++ b/mercurial/dirstate.py
@@ -408,32 +408,48 @@
             self._droppath(f)
             del self._map[f]
 
-    def _normalize(self, path, isknown):
+    def _normalize(self, path, isknown, ignoremissing=False, exists=None):
         normed = util.normcase(path)
         folded = self._foldmap.get(normed, None)
         if folded is None:
-            if isknown or not os.path.lexists(os.path.join(self._root, path)):
+            if isknown:
                 folded = path
             else:
-                # recursively normalize leading directory components
-                # against dirstate
-                if '/' in normed:
-                    d, f = normed.rsplit('/', 1)
-                    d = self._normalize(d, isknown)
-                    r = self._root + "/" + d
-                    folded = d + "/" + util.fspath(f, r)
+                if exists is None:
+                    exists = os.path.lexists(os.path.join(self._root, path))
+                if not exists:
+                    # Maybe a path component exists
+                    if not ignoremissing and '/' in path:
+                        d, f = path.rsplit('/', 1)
+                        d = self._normalize(d, isknown, ignoremissing, None)
+                        folded = d + "/" + f
+                    else:
+                        # No path components, preserve original case
+                        folded = path
                 else:
-                    folded = util.fspath(normed, self._root)
-                self._foldmap[normed] = folded
+                    # recursively normalize leading directory components
+                    # against dirstate
+                    if '/' in normed:
+                        d, f = normed.rsplit('/', 1)
+                        d = self._normalize(d, isknown, ignoremissing, True)
+                        r = self._root + "/" + d
+                        folded = d + "/" + util.fspath(f, r)
+                    else:
+                        folded = util.fspath(normed, self._root)
+                    self._foldmap[normed] = folded
 
         return folded
 
-    def normalize(self, path, isknown=False):
+    def normalize(self, path, isknown=False, ignoremissing=False):
         '''
         normalize the case of a pathname when on a casefolding filesystem
 
         isknown specifies whether the filename came from walking the
-        disk, to avoid extra filesystem access
+        disk, to avoid extra filesystem access.
+
+        If ignoremissing is True, missing path are returned
+        unchanged. Otherwise, we try harder to normalize possibly
+        existing path components.
 
         The normalized case is determined based on the following precedence:
 
@@ -443,7 +459,7 @@
         '''
 
         if self._checkcase:
-            return self._normalize(path, isknown)
+            return self._normalize(path, isknown, ignoremissing)
         return path
 
     def clear(self):
@@ -575,7 +591,7 @@
             normalize = self._normalize
             skipstep3 = False
         else:
-            normalize = lambda x, y: x
+            normalize = lambda x, y, z: x
 
         files = sorted(match.files())
         subrepos.sort()
@@ -596,7 +612,7 @@
 
         # step 1: find all explicit files
         for ff in files:
-            nf = normalize(normpath(ff), False)
+            nf = normalize(normpath(ff), False, True)
             if nf in results:
                 continue
 
@@ -646,7 +662,7 @@
                     continue
                 raise
             for f, kind, st in entries:
-                nf = normalize(nd and (nd + "/" + f) or f, True)
+                nf = normalize(nd and (nd + "/" + f) or f, True, True)
                 if nf not in results:
                     if kind == dirkind:
                         if not ignore(nf):
diff --git a/tests/test-casefolding.t b/tests/test-casefolding.t
--- a/tests/test-casefolding.t
+++ b/tests/test-casefolding.t
@@ -32,6 +32,42 @@
   $ hg mv a A
   $ hg mv A a
   $ hg st
+
+test changing case of path components
+
+  $ mkdir D
+  $ echo b > D/b
+  $ hg ci -Am addb D/b
+  $ hg mv D/b d/b
+  D/b: not overwriting - file exists
+  $ hg mv D/b d/c
+  $ hg st
+  A D/c
+  R D/b
+  $ mv D temp
+  $ mv temp d
+  $ hg st
+  A D/c
+  R D/b
+  $ hg revert -aq
+  $ rm d/c
+  $ echo c > D/c
+  $ hg add D/c
+  $ hg st
+  A D/c
+  $ hg ci -m addc D/c
+  $ hg mv d/b d/e
+  moving D/b to D/e
+  $ hg st
+  A D/e
+  R D/b
+  $ hg revert -aq
+  $ rm d/e
+  $ hg mv d/b D/B
+  moving D/b to D/B
+  $ hg st
+  A D/B
+  R D/b
   $ cd ..
 
 test case collision between revisions (issue912)


More information about the Mercurial-devel mailing list