comparison tests/test-commit-multiple.t @ 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
children 8bb03283e9b9
comparison
equal deleted inserted replaced
13703:48d606d7192b 13704:a464763e99f1
1 # reproduce issue2264, issue2516
2
3 create test repo
4 $ cat <<EOF >> $HGRCPATH
5 > [extensions]
6 > transplant =
7 > graphlog =
8 > EOF
9 $ hg init repo
10 $ cd repo
11 $ template="{rev} {desc|firstline} [{branch}]\n"
12
13 # we need to start out with two changesets on the default branch
14 # in order to avoid the cute little optimization where transplant
15 # pulls rather than transplants
16 add initial changesets
17 $ echo feature1 > file1
18 $ hg ci -Am"feature 1"
19 adding file1
20 $ echo feature2 >> file2
21 $ hg ci -Am"feature 2"
22 adding file2
23
24 # The changes to 'bugfix' are enough to show the bug: in fact, with only
25 # those changes, it's a very noisy crash ("RuntimeError: nothing
26 # committed after transplant"). But if we modify a second file in the
27 # transplanted changesets, the bug is much more subtle: transplant
28 # silently drops the second change to 'bugfix' on the floor, and we only
29 # see it when we run 'hg status' after transplanting. Subtle data loss
30 # bugs are worse than crashes, so reproduce the subtle case here.
31 commit bug fixes on bug fix branch
32 $ hg branch fixes
33 marked working directory as branch fixes
34 $ echo fix1 > bugfix
35 $ echo fix1 >> file1
36 $ hg ci -Am"fix 1"
37 adding bugfix
38 $ echo fix2 > bugfix
39 $ echo fix2 >> file1
40 $ hg ci -Am"fix 2"
41 $ hg glog --template="$template"
42 @ 3 fix 2 [fixes]
43 |
44 o 2 fix 1 [fixes]
45 |
46 o 1 feature 2 [default]
47 |
48 o 0 feature 1 [default]
49
50 transplant bug fixes onto release branch
51 $ hg update 0
52 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
53 $ hg branch release
54 marked working directory as branch release
55 $ hg transplant 2 3
56 applying [0-9a-f]{12} (re)
57 [0-9a-f]{12} transplanted to [0-9a-f]{12} (re)
58 applying [0-9a-f]{12} (re)
59 [0-9a-f]{12} transplanted to [0-9a-f]{12} (re)
60 $ hg glog --template="$template"
61 @ 5 fix 2 [release]
62 |
63 o 4 fix 1 [release]
64 |
65 | o 3 fix 2 [fixes]
66 | |
67 | o 2 fix 1 [fixes]
68 | |
69 | o 1 feature 2 [default]
70 |/
71 o 0 feature 1 [default]
72
73 $ hg status
74 $ hg status --rev 0:4
75 M file1
76 A bugfix
77 $ hg status --rev 4:5
78 M bugfix
79 M file1
80
81 now test that we fixed the bug for all scripts/extensions
82 $ cat > $TESTTMP/committwice.py <<__EOF__
83 > from mercurial import ui, hg, match, node
84 >
85 > def replacebyte(fn, b):
86 > f = open("file1", "rb+")
87 > f.seek(0, 0)
88 > f.write(b)
89 > f.close()
90 >
91 > repo = hg.repository(ui.ui(), '.')
92 > assert len(repo) == 6, \
93 > "initial: len(repo) == %d, expected 6" % len(repo)
94 > try:
95 > wlock = repo.wlock()
96 > lock = repo.lock()
97 > m = match.exact(repo.root, '', ['file1'])
98 > replacebyte("file1", "x")
99 > n = repo.commit(text="x", user="test", date=(0, 0), match=m)
100 > print "commit 1: len(repo) == %d" % len(repo)
101 > replacebyte("file1", "y")
102 > n = repo.commit(text="y", user="test", date=(0, 0), match=m)
103 > print "commit 2: len(repo) == %d" % len(repo)
104 > finally:
105 > lock.release()
106 > wlock.release()
107 > __EOF__
108 $ $PYTHON $TESTTMP/committwice.py
109 commit 1: len(repo) == 7
110 commit 2: len(repo) == 8
111
112 Do a size-preserving modification outside of that process
113 $ echo abcd > bugfix
114 $ hg status
115 M bugfix
116 $ hg log --template "{rev} {desc} {files}\n" -r5:
117 5 fix 2 bugfix file1
118 6 x file1
119 7 y file1