Mercurial > public > mercurial-scm > hg
diff mercurial/dirstate.py @ 13704:a464763e99f1
dirstate: avoid a race with multiple commits in the same process
(issue2264, issue2516)
The race happens when two commits in a row change the same file
without changing its size, *if* those two commits happen in the same
second in the same process while holding the same repo lock. For
example:
commit 1:
M a
M b
commit 2: # same process, same second, same repo lock
M b # modify b without changing its size
M c
This first manifested in transplant, which is the most common way to
do multiple commits in the same process. But it can manifest in any
script or extension that does multiple commits under the same repo
lock. (Thus, the test script tests both transplant and a custom script.)
The problem was that dirstate.status() failed to notice the change to
b when localrepo is about to do the second commit, meaning that change
gets left in the working directory. In the context of transplant, that
means either a crash ("RuntimeError: nothing committed after
transplant") or a silently inaccurate transplant, depending on whether
any other files were modified by the second transplanted changeset.
The fix is to make status() work a little harder when we have
previously marked files as clean (state 'normal') in the same process.
Specifically, dirstate.normal() adds files to self._lastnormal, and
other state-changing methods remove them. Then dirstate.status() puts
any files in self._lastnormal into state 'lookup', which will make
localrepository.status() read file contents to see if it has really
changed. So we pay a small performance penalty for the second (and
subsequent) commits in the same process, without affecting the common
case. Anything that does lots of status updates and checks in the
same process could suffer a performance hit.
Incidentally, there is a simpler fix: call dirstate.normallookup() on
every file updated by commit() at the end of the commit. The trouble
with that solution is that it imposes a performance penalty on the
common case: it means the next status-dependent hg command after every
"hg commit" will be a little bit slower. The patch here is more
complex, but only affects performance for the uncommon case.
author | Greg Ward <greg@gerg.ca> |
---|---|
date | Sun, 20 Mar 2011 17:41:09 -0400 |
parents | 14f3795a5ed7 |
children | 15d1db2abfcb |
line wrap: on
line diff
--- a/mercurial/dirstate.py Sun Mar 20 01:16:57 2011 +0100 +++ b/mercurial/dirstate.py Sun Mar 20 17:41:09 2011 -0400 @@ -49,6 +49,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ radd = removed.append dadd = deleted.append cadd = clean.append + lastnormal = self._lastnormal.__contains__ lnkkind = stat.S_IFLNK @@ -672,6 +686,18 @@ 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':