# HG changeset patch # User Patrick Mezard # Date 2012-04-28 18:29:21 # Node ID e596a631210e2b6a28669aabb27c0d874e5d4c3a # Parent bb3334806ace20bb8c3c214102e42b9472bac258 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 @@ def copy(ui, repo, pats, opts, rename=Fa # 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 @@ class dirstate(object): 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 @@ class dirstate(object): ''' if self._checkcase: - return self._normalize(path, isknown) + return self._normalize(path, isknown, ignoremissing) return path def clear(self): @@ -575,7 +591,7 @@ class dirstate(object): 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 @@ class dirstate(object): # 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 @@ class dirstate(object): 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 @@ Case-changing renames should work: $ 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)