diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py --- a/mercurial/dirstate.py +++ b/mercurial/dirstate.py @@ -49,6 +49,7 @@ class dirstate(object): self._rootdir = os.path.join(root, '') self._dirty = False self._dirtypl = False + self._lastnormal = set() # files believed to be normal self._ui = ui @propertycache @@ -285,6 +286,12 @@ class dirstate(object): if f in self._copymap: del self._copymap[f] + # Right now, this file is clean: but if some code in this + # process modifies it without changing its size before the clock + # ticks over to the next second, then it won't be clean anymore. + # So make sure that status() will look harder at it. + self._lastnormal.add(f) + def normallookup(self, f): '''Mark a file normal, but possibly dirty.''' if self._pl[1] != nullid and f in self._map: @@ -308,6 +315,7 @@ class dirstate(object): self._map[f] = ('n', 0, -1, -1) if f in self._copymap: del self._copymap[f] + self._lastnormal.discard(f) def otherparent(self, f): '''Mark as coming from the other parent, always dirty.''' @@ -319,6 +327,7 @@ class dirstate(object): self._map[f] = ('n', 0, -2, -1) if f in self._copymap: del self._copymap[f] + self._lastnormal.discard(f) def add(self, f): '''Mark a file added.''' @@ -327,6 +336,7 @@ class dirstate(object): self._map[f] = ('a', 0, -1, -1) if f in self._copymap: del self._copymap[f] + self._lastnormal.discard(f) def remove(self, f): '''Mark a file removed.''' @@ -343,6 +353,7 @@ class dirstate(object): self._map[f] = ('r', 0, size, 0) if size == 0 and f in self._copymap: del self._copymap[f] + self._lastnormal.discard(f) def merge(self, f): '''Mark a file merged.''' @@ -352,6 +363,7 @@ class dirstate(object): self._map[f] = ('m', s.st_mode, s.st_size, int(s.st_mtime)) if f in self._copymap: del self._copymap[f] + self._lastnormal.discard(f) def forget(self, f): '''Forget a file.''' @@ -361,6 +373,7 @@ class dirstate(object): del self._map[f] except KeyError: self._ui.warn(_("not in dirstate: %s\n") % f) + self._lastnormal.discard(f) def _normalize(self, path, knownpath): norm_path = os.path.normcase(path) @@ -640,6 +653,7 @@ class dirstate(object): radd = removed.append dadd = deleted.append cadd = clean.append + lastnormal = self._lastnormal.__contains__ lnkkind = stat.S_IFLNK @@ -672,6 +686,18 @@ class dirstate(object): elif (time != int(st.st_mtime) and (mode & lnkkind != lnkkind or self._checklink)): ladd(fn) + elif lastnormal(fn): + # If previously in this process we recorded that + # this file is clean, think twice: intervening code + # may have modified the file in the same second + # without changing its size. So force caller to + # check file contents. Because we're not updating + # self._map, this only affects the current process. + # That should be OK because this mainly affects + # multiple commits in the same process, and each + # commit by definition makes the committed files + # clean. + ladd(fn) elif listclean: cadd(fn) elif state == 'm': diff --git a/tests/test-commit-multiple.t b/tests/test-commit-multiple.t new file mode 100644 --- /dev/null +++ b/tests/test-commit-multiple.t @@ -0,0 +1,119 @@ +# reproduce issue2264, issue2516 + +create test repo + $ cat <> $HGRCPATH + > [extensions] + > transplant = + > graphlog = + > EOF + $ hg init repo + $ cd repo + $ template="{rev} {desc|firstline} [{branch}]\n" + +# we need to start out with two changesets on the default branch +# in order to avoid the cute little optimization where transplant +# pulls rather than transplants +add initial changesets + $ echo feature1 > file1 + $ hg ci -Am"feature 1" + adding file1 + $ echo feature2 >> file2 + $ hg ci -Am"feature 2" + adding file2 + +# The changes to 'bugfix' are enough to show the bug: in fact, with only +# those changes, it's a very noisy crash ("RuntimeError: nothing +# committed after transplant"). But if we modify a second file in the +# transplanted changesets, the bug is much more subtle: transplant +# silently drops the second change to 'bugfix' on the floor, and we only +# see it when we run 'hg status' after transplanting. Subtle data loss +# bugs are worse than crashes, so reproduce the subtle case here. +commit bug fixes on bug fix branch + $ hg branch fixes + marked working directory as branch fixes + $ echo fix1 > bugfix + $ echo fix1 >> file1 + $ hg ci -Am"fix 1" + adding bugfix + $ echo fix2 > bugfix + $ echo fix2 >> file1 + $ hg ci -Am"fix 2" + $ hg glog --template="$template" + @ 3 fix 2 [fixes] + | + o 2 fix 1 [fixes] + | + o 1 feature 2 [default] + | + o 0 feature 1 [default] + +transplant bug fixes onto release branch + $ hg update 0 + 1 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg branch release + marked working directory as branch release + $ hg transplant 2 3 + applying [0-9a-f]{12} (re) + [0-9a-f]{12} transplanted to [0-9a-f]{12} (re) + applying [0-9a-f]{12} (re) + [0-9a-f]{12} transplanted to [0-9a-f]{12} (re) + $ hg glog --template="$template" + @ 5 fix 2 [release] + | + o 4 fix 1 [release] + | + | o 3 fix 2 [fixes] + | | + | o 2 fix 1 [fixes] + | | + | o 1 feature 2 [default] + |/ + o 0 feature 1 [default] + + $ hg status + $ hg status --rev 0:4 + M file1 + A bugfix + $ hg status --rev 4:5 + M bugfix + M file1 + +now test that we fixed the bug for all scripts/extensions + $ cat > $TESTTMP/committwice.py <<__EOF__ + > from mercurial import ui, hg, match, node + > + > def replacebyte(fn, b): + > f = open("file1", "rb+") + > f.seek(0, 0) + > f.write(b) + > f.close() + > + > repo = hg.repository(ui.ui(), '.') + > assert len(repo) == 6, \ + > "initial: len(repo) == %d, expected 6" % len(repo) + > try: + > wlock = repo.wlock() + > lock = repo.lock() + > m = match.exact(repo.root, '', ['file1']) + > replacebyte("file1", "x") + > n = repo.commit(text="x", user="test", date=(0, 0), match=m) + > print "commit 1: len(repo) == %d" % len(repo) + > replacebyte("file1", "y") + > n = repo.commit(text="y", user="test", date=(0, 0), match=m) + > print "commit 2: len(repo) == %d" % len(repo) + > finally: + > lock.release() + > wlock.release() + > __EOF__ + $ $PYTHON $TESTTMP/committwice.py + commit 1: len(repo) == 7 + commit 2: len(repo) == 8 + +Do a size-preserving modification outside of that process + $ echo abcd > bugfix + $ hg status + M bugfix + $ hg log --template "{rev} {desc} {files}\n" -r5: + 5 fix 2 bugfix file1 + 6 x file1 + 7 y file1