Fix file-changed-to-dir and dir-to-file commits (issue660).

Allow adding to dirstate files that clash with previously existing
but marked for removal. Protect from reintroducing clashes by revert.

This change doesn't address related issues with update. Current
workaround is to do "clean" update by manually removing conflicting
files/dirs from working directory.
This commit is contained in:
Maxim Dounin 2007-11-05 20:05:44 +03:00
parent 49a794484a
commit 8561d688a1
5 changed files with 158 additions and 9 deletions

View File

@ -266,14 +266,15 @@ def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
mapping[abs] = rel, exact
if repo.ui.verbose or not exact:
repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
if repo.dirstate[abs] != 'r' and not util.lexists(target):
if repo.dirstate[abs] != 'r' and (not util.lexists(target)
or (os.path.isdir(target) and not os.path.islink(target))):
remove.append(abs)
mapping[abs] = rel, exact
if repo.ui.verbose or not exact:
repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
if not dry_run:
repo.add(add)
repo.remove(remove)
repo.add(add)
if similarity > 0:
for old, new, score in findrenames(repo, add, remove, similarity):
oldrel, oldexact = mapping[old]

View File

@ -50,7 +50,8 @@ class dirstate(object):
elif name == '_dirs':
self._dirs = {}
for f in self._map:
self._incpath(f)
if self[f] != 'r':
self._incpath(f)
return self._dirs
elif name == '_ignore':
files = [self._join('.hgignore')]
@ -205,14 +206,25 @@ class dirstate(object):
d = f[:c]
if d in self._dirs:
break
if d in self._map:
if d in self._map and self[d] != 'r':
raise util.Abort(_('file %r in dirstate clashes with %r') %
(d, f))
self._incpath(f)
def _changepath(self, f, newstate):
# handle upcoming path changes
oldstate = self[f]
if oldstate not in "?r" and newstate in "?r":
self._decpath(f)
return
if oldstate in "?r" and newstate not in "?r":
self._incpathcheck(f)
return
def normal(self, f):
'mark a file normal and clean'
self._dirty = True
self._changepath(f, 'n')
s = os.lstat(self._join(f))
self._map[f] = ('n', s.st_mode, s.st_size, s.st_mtime, 0)
if self._copymap.has_key(f):
@ -221,6 +233,7 @@ class dirstate(object):
def normallookup(self, f):
'mark a file normal, but possibly dirty'
self._dirty = True
self._changepath(f, 'n')
self._map[f] = ('n', 0, -1, -1, 0)
if f in self._copymap:
del self._copymap[f]
@ -228,6 +241,7 @@ class dirstate(object):
def normaldirty(self, f):
'mark a file normal, but dirty'
self._dirty = True
self._changepath(f, 'n')
self._map[f] = ('n', 0, -2, -1, 0)
if f in self._copymap:
del self._copymap[f]
@ -235,7 +249,7 @@ class dirstate(object):
def add(self, f):
'mark a file added'
self._dirty = True
self._incpathcheck(f)
self._changepath(f, 'a')
self._map[f] = ('a', 0, -1, -1, 0)
if f in self._copymap:
del self._copymap[f]
@ -243,8 +257,8 @@ class dirstate(object):
def remove(self, f):
'mark a file removed'
self._dirty = True
self._changepath(f, 'r')
self._map[f] = ('r', 0, 0, 0, 0)
self._decpath(f)
if f in self._copymap:
del self._copymap[f]
@ -252,6 +266,7 @@ class dirstate(object):
'mark a file merged'
self._dirty = True
s = os.lstat(self._join(f))
self._changepath(f, 'm')
self._map[f] = ('m', s.st_mode, s.st_size, s.st_mtime, 0)
if f in self._copymap:
del self._copymap[f]
@ -260,13 +275,15 @@ class dirstate(object):
'forget a file'
self._dirty = True
try:
self._changepath(f, '?')
del self._map[f]
self._decpath(f)
except KeyError:
self._ui.warn(_("not in dirstate: %s!\n") % f)
def clear(self):
self._map = {}
if "_dirs" in self.__dict__:
delattr(self, "_dirs");
self._copymap = {}
self._pl = [nullid, nullid]
self._dirty = True
@ -522,7 +539,7 @@ class dirstate(object):
try:
st = lstat(_join(fn))
except OSError, inst:
if inst.errno != errno.ENOENT:
if inst.errno not in (errno.ENOENT, errno.ENOTDIR):
raise
st = None
# We need to re-check that it is a valid file

View File

@ -720,7 +720,7 @@ class path_auditor(object):
except OSError, err:
# EINVAL can be raised as invalid path syntax under win32.
# They must be ignored for patterns can be checked too.
if err.errno not in (errno.ENOENT, errno.EINVAL):
if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
raise
else:
if stat.S_ISLNK(st.st_mode):

89
tests/test-issue660 Executable file
View File

@ -0,0 +1,89 @@
#!/bin/sh
# http://www.selenic.com/mercurial/bts/issue660
hg init a
cd a
echo a > a
mkdir b
echo b > b/b
hg commit -A -m "a is file, b is dir"
echo % file replaced with directory
rm a
mkdir a
echo a > a/a
echo % should fail - would corrupt dirstate
hg add a/a
echo % removing shadow
hg rm --after a
echo % should succeed - shadow removed
hg add a/a
echo % directory replaced with file
rm -r b
echo b > b
echo % should fail - would corrupt dirstate
hg add b
echo % removing shadow
hg rm --after b/b
echo % should succeed - shadow removed
hg add b
echo % look what we got
hg st
echo % revert reintroducing shadow - should fail
rm -r a b
hg revert b/b
echo % revert all - should succeed
hg revert --all
hg st
echo % addremove
rm -r a b
mkdir a
echo a > a/a
echo b > b
hg addremove
hg st
echo % commit
hg ci -A -m "a is dir, b is file"
hg st --all
echo % long directory replaced with file
mkdir d
mkdir d/d
echo d > d/d/d
hg commit -A -m "d is long directory"
rm -r d
echo d > d
echo % should fail - would corrupt dirstate
hg add d
echo % removing shadow
hg rm --after d/d/d
echo % should succeed - shadow removed
hg add d
#echo % update should work
#
#hg up -r 0
#hg up -r 1
exit 0

42
tests/test-issue660.out Normal file
View File

@ -0,0 +1,42 @@
adding a
adding b/b
% file replaced with directory
% should fail - would corrupt dirstate
abort: file 'a' in dirstate clashes with 'a/a'
% removing shadow
% should succeed - shadow removed
% directory replaced with file
% should fail - would corrupt dirstate
abort: directory 'b' already in dirstate
% removing shadow
% should succeed - shadow removed
% look what we got
A a/a
A b
R a
R b/b
% revert reintroducing shadow - should fail
abort: file 'b' in dirstate clashes with 'b/b'
% revert all - should succeed
undeleting a
forgetting a/a
forgetting b
undeleting b/b
% addremove
removing a
adding a/a
adding b
removing b/b
A a/a
A b
R a
R b/b
% commit
C a/a
C b
% long directory replaced with file
adding d/d/d
% should fail - would corrupt dirstate
abort: directory 'd' already in dirstate
% removing shadow
% should succeed - shadow removed