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.
--- a/mercurial/cmdutil.py Sat Apr 28 02:00:04 2012 +0200
+++ b/mercurial/cmdutil.py Sat Apr 28 20:29:21 2012 +0200
@@ -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)
--- a/mercurial/dirstate.py Sat Apr 28 02:00:04 2012 +0200
+++ b/mercurial/dirstate.py Sat Apr 28 20:29:21 2012 +0200
@@ -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):
--- a/tests/test-casefolding.t Sat Apr 28 02:00:04 2012 +0200
+++ b/tests/test-casefolding.t Sat Apr 28 20:29:21 2012 +0200
@@ -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)