Show More
@@ -0,0 +1,119 b'' | |||||
|
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 |
@@ -49,6 +49,7 b' class dirstate(object):' | |||||
49 | self._rootdir = os.path.join(root, '') |
|
49 | self._rootdir = os.path.join(root, '') | |
50 | self._dirty = False |
|
50 | self._dirty = False | |
51 | self._dirtypl = False |
|
51 | self._dirtypl = False | |
|
52 | self._lastnormal = set() # files believed to be normal | |||
52 | self._ui = ui |
|
53 | self._ui = ui | |
53 |
|
54 | |||
54 | @propertycache |
|
55 | @propertycache | |
@@ -285,6 +286,12 b' class dirstate(object):' | |||||
285 | if f in self._copymap: |
|
286 | if f in self._copymap: | |
286 | del self._copymap[f] |
|
287 | del self._copymap[f] | |
287 |
|
288 | |||
|
289 | # Right now, this file is clean: but if some code in this | |||
|
290 | # process modifies it without changing its size before the clock | |||
|
291 | # ticks over to the next second, then it won't be clean anymore. | |||
|
292 | # So make sure that status() will look harder at it. | |||
|
293 | self._lastnormal.add(f) | |||
|
294 | ||||
288 | def normallookup(self, f): |
|
295 | def normallookup(self, f): | |
289 | '''Mark a file normal, but possibly dirty.''' |
|
296 | '''Mark a file normal, but possibly dirty.''' | |
290 | if self._pl[1] != nullid and f in self._map: |
|
297 | if self._pl[1] != nullid and f in self._map: | |
@@ -308,6 +315,7 b' class dirstate(object):' | |||||
308 | self._map[f] = ('n', 0, -1, -1) |
|
315 | self._map[f] = ('n', 0, -1, -1) | |
309 | if f in self._copymap: |
|
316 | if f in self._copymap: | |
310 | del self._copymap[f] |
|
317 | del self._copymap[f] | |
|
318 | self._lastnormal.discard(f) | |||
311 |
|
319 | |||
312 | def otherparent(self, f): |
|
320 | def otherparent(self, f): | |
313 | '''Mark as coming from the other parent, always dirty.''' |
|
321 | '''Mark as coming from the other parent, always dirty.''' | |
@@ -319,6 +327,7 b' class dirstate(object):' | |||||
319 | self._map[f] = ('n', 0, -2, -1) |
|
327 | self._map[f] = ('n', 0, -2, -1) | |
320 | if f in self._copymap: |
|
328 | if f in self._copymap: | |
321 | del self._copymap[f] |
|
329 | del self._copymap[f] | |
|
330 | self._lastnormal.discard(f) | |||
322 |
|
331 | |||
323 | def add(self, f): |
|
332 | def add(self, f): | |
324 | '''Mark a file added.''' |
|
333 | '''Mark a file added.''' | |
@@ -327,6 +336,7 b' class dirstate(object):' | |||||
327 | self._map[f] = ('a', 0, -1, -1) |
|
336 | self._map[f] = ('a', 0, -1, -1) | |
328 | if f in self._copymap: |
|
337 | if f in self._copymap: | |
329 | del self._copymap[f] |
|
338 | del self._copymap[f] | |
|
339 | self._lastnormal.discard(f) | |||
330 |
|
340 | |||
331 | def remove(self, f): |
|
341 | def remove(self, f): | |
332 | '''Mark a file removed.''' |
|
342 | '''Mark a file removed.''' | |
@@ -343,6 +353,7 b' class dirstate(object):' | |||||
343 | self._map[f] = ('r', 0, size, 0) |
|
353 | self._map[f] = ('r', 0, size, 0) | |
344 | if size == 0 and f in self._copymap: |
|
354 | if size == 0 and f in self._copymap: | |
345 | del self._copymap[f] |
|
355 | del self._copymap[f] | |
|
356 | self._lastnormal.discard(f) | |||
346 |
|
357 | |||
347 | def merge(self, f): |
|
358 | def merge(self, f): | |
348 | '''Mark a file merged.''' |
|
359 | '''Mark a file merged.''' | |
@@ -352,6 +363,7 b' class dirstate(object):' | |||||
352 | self._map[f] = ('m', s.st_mode, s.st_size, int(s.st_mtime)) |
|
363 | self._map[f] = ('m', s.st_mode, s.st_size, int(s.st_mtime)) | |
353 | if f in self._copymap: |
|
364 | if f in self._copymap: | |
354 | del self._copymap[f] |
|
365 | del self._copymap[f] | |
|
366 | self._lastnormal.discard(f) | |||
355 |
|
367 | |||
356 | def forget(self, f): |
|
368 | def forget(self, f): | |
357 | '''Forget a file.''' |
|
369 | '''Forget a file.''' | |
@@ -361,6 +373,7 b' class dirstate(object):' | |||||
361 | del self._map[f] |
|
373 | del self._map[f] | |
362 | except KeyError: |
|
374 | except KeyError: | |
363 | self._ui.warn(_("not in dirstate: %s\n") % f) |
|
375 | self._ui.warn(_("not in dirstate: %s\n") % f) | |
|
376 | self._lastnormal.discard(f) | |||
364 |
|
377 | |||
365 | def _normalize(self, path, knownpath): |
|
378 | def _normalize(self, path, knownpath): | |
366 | norm_path = os.path.normcase(path) |
|
379 | norm_path = os.path.normcase(path) | |
@@ -640,6 +653,7 b' class dirstate(object):' | |||||
640 | radd = removed.append |
|
653 | radd = removed.append | |
641 | dadd = deleted.append |
|
654 | dadd = deleted.append | |
642 | cadd = clean.append |
|
655 | cadd = clean.append | |
|
656 | lastnormal = self._lastnormal.__contains__ | |||
643 |
|
657 | |||
644 | lnkkind = stat.S_IFLNK |
|
658 | lnkkind = stat.S_IFLNK | |
645 |
|
659 | |||
@@ -672,6 +686,18 b' class dirstate(object):' | |||||
672 | elif (time != int(st.st_mtime) |
|
686 | elif (time != int(st.st_mtime) | |
673 | and (mode & lnkkind != lnkkind or self._checklink)): |
|
687 | and (mode & lnkkind != lnkkind or self._checklink)): | |
674 | ladd(fn) |
|
688 | ladd(fn) | |
|
689 | elif lastnormal(fn): | |||
|
690 | # If previously in this process we recorded that | |||
|
691 | # this file is clean, think twice: intervening code | |||
|
692 | # may have modified the file in the same second | |||
|
693 | # without changing its size. So force caller to | |||
|
694 | # check file contents. Because we're not updating | |||
|
695 | # self._map, this only affects the current process. | |||
|
696 | # That should be OK because this mainly affects | |||
|
697 | # multiple commits in the same process, and each | |||
|
698 | # commit by definition makes the committed files | |||
|
699 | # clean. | |||
|
700 | ladd(fn) | |||
675 | elif listclean: |
|
701 | elif listclean: | |
676 | cadd(fn) |
|
702 | cadd(fn) | |
677 | elif state == 'm': |
|
703 | elif state == 'm': |
General Comments 0
You need to be logged in to leave comments.
Login now