##// END OF EJS Templates
dirstate: avoid a race with multiple commits in the same process...
Greg Ward -
r13704:a464763e default
parent child Browse files
Show More
@@ -0,0 +1,119
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
@@ -1,685 +1,711
1 # dirstate.py - working directory tracking for mercurial
1 # dirstate.py - working directory tracking for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from node import nullid
8 from node import nullid
9 from i18n import _
9 from i18n import _
10 import util, ignore, osutil, parsers, encoding
10 import util, ignore, osutil, parsers, encoding
11 import struct, os, stat, errno
11 import struct, os, stat, errno
12 import cStringIO
12 import cStringIO
13
13
14 _format = ">cllll"
14 _format = ">cllll"
15 propertycache = util.propertycache
15 propertycache = util.propertycache
16
16
17 def _finddirs(path):
17 def _finddirs(path):
18 pos = path.rfind('/')
18 pos = path.rfind('/')
19 while pos != -1:
19 while pos != -1:
20 yield path[:pos]
20 yield path[:pos]
21 pos = path.rfind('/', 0, pos)
21 pos = path.rfind('/', 0, pos)
22
22
23 def _incdirs(dirs, path):
23 def _incdirs(dirs, path):
24 for base in _finddirs(path):
24 for base in _finddirs(path):
25 if base in dirs:
25 if base in dirs:
26 dirs[base] += 1
26 dirs[base] += 1
27 return
27 return
28 dirs[base] = 1
28 dirs[base] = 1
29
29
30 def _decdirs(dirs, path):
30 def _decdirs(dirs, path):
31 for base in _finddirs(path):
31 for base in _finddirs(path):
32 if dirs[base] > 1:
32 if dirs[base] > 1:
33 dirs[base] -= 1
33 dirs[base] -= 1
34 return
34 return
35 del dirs[base]
35 del dirs[base]
36
36
37 class dirstate(object):
37 class dirstate(object):
38
38
39 def __init__(self, opener, ui, root, validate):
39 def __init__(self, opener, ui, root, validate):
40 '''Create a new dirstate object.
40 '''Create a new dirstate object.
41
41
42 opener is an open()-like callable that can be used to open the
42 opener is an open()-like callable that can be used to open the
43 dirstate file; root is the root of the directory tracked by
43 dirstate file; root is the root of the directory tracked by
44 the dirstate.
44 the dirstate.
45 '''
45 '''
46 self._opener = opener
46 self._opener = opener
47 self._validate = validate
47 self._validate = validate
48 self._root = root
48 self._root = root
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
55 def _map(self):
56 def _map(self):
56 '''Return the dirstate contents as a map from filename to
57 '''Return the dirstate contents as a map from filename to
57 (state, mode, size, time).'''
58 (state, mode, size, time).'''
58 self._read()
59 self._read()
59 return self._map
60 return self._map
60
61
61 @propertycache
62 @propertycache
62 def _copymap(self):
63 def _copymap(self):
63 self._read()
64 self._read()
64 return self._copymap
65 return self._copymap
65
66
66 @propertycache
67 @propertycache
67 def _foldmap(self):
68 def _foldmap(self):
68 f = {}
69 f = {}
69 for name in self._map:
70 for name in self._map:
70 f[os.path.normcase(name)] = name
71 f[os.path.normcase(name)] = name
71 return f
72 return f
72
73
73 @propertycache
74 @propertycache
74 def _branch(self):
75 def _branch(self):
75 try:
76 try:
76 return self._opener("branch").read().strip() or "default"
77 return self._opener("branch").read().strip() or "default"
77 except IOError:
78 except IOError:
78 return "default"
79 return "default"
79
80
80 @propertycache
81 @propertycache
81 def _pl(self):
82 def _pl(self):
82 try:
83 try:
83 fp = self._opener("dirstate")
84 fp = self._opener("dirstate")
84 st = fp.read(40)
85 st = fp.read(40)
85 fp.close()
86 fp.close()
86 l = len(st)
87 l = len(st)
87 if l == 40:
88 if l == 40:
88 return st[:20], st[20:40]
89 return st[:20], st[20:40]
89 elif l > 0 and l < 40:
90 elif l > 0 and l < 40:
90 raise util.Abort(_('working directory state appears damaged!'))
91 raise util.Abort(_('working directory state appears damaged!'))
91 except IOError, err:
92 except IOError, err:
92 if err.errno != errno.ENOENT:
93 if err.errno != errno.ENOENT:
93 raise
94 raise
94 return [nullid, nullid]
95 return [nullid, nullid]
95
96
96 @propertycache
97 @propertycache
97 def _dirs(self):
98 def _dirs(self):
98 dirs = {}
99 dirs = {}
99 for f, s in self._map.iteritems():
100 for f, s in self._map.iteritems():
100 if s[0] != 'r':
101 if s[0] != 'r':
101 _incdirs(dirs, f)
102 _incdirs(dirs, f)
102 return dirs
103 return dirs
103
104
104 @propertycache
105 @propertycache
105 def _ignore(self):
106 def _ignore(self):
106 files = [self._join('.hgignore')]
107 files = [self._join('.hgignore')]
107 for name, path in self._ui.configitems("ui"):
108 for name, path in self._ui.configitems("ui"):
108 if name == 'ignore' or name.startswith('ignore.'):
109 if name == 'ignore' or name.startswith('ignore.'):
109 files.append(util.expandpath(path))
110 files.append(util.expandpath(path))
110 return ignore.ignore(self._root, files, self._ui.warn)
111 return ignore.ignore(self._root, files, self._ui.warn)
111
112
112 @propertycache
113 @propertycache
113 def _slash(self):
114 def _slash(self):
114 return self._ui.configbool('ui', 'slash') and os.sep != '/'
115 return self._ui.configbool('ui', 'slash') and os.sep != '/'
115
116
116 @propertycache
117 @propertycache
117 def _checklink(self):
118 def _checklink(self):
118 return util.checklink(self._root)
119 return util.checklink(self._root)
119
120
120 @propertycache
121 @propertycache
121 def _checkexec(self):
122 def _checkexec(self):
122 return util.checkexec(self._root)
123 return util.checkexec(self._root)
123
124
124 @propertycache
125 @propertycache
125 def _checkcase(self):
126 def _checkcase(self):
126 return not util.checkcase(self._join('.hg'))
127 return not util.checkcase(self._join('.hg'))
127
128
128 def _join(self, f):
129 def _join(self, f):
129 # much faster than os.path.join()
130 # much faster than os.path.join()
130 # it's safe because f is always a relative path
131 # it's safe because f is always a relative path
131 return self._rootdir + f
132 return self._rootdir + f
132
133
133 def flagfunc(self, fallback):
134 def flagfunc(self, fallback):
134 if self._checklink:
135 if self._checklink:
135 if self._checkexec:
136 if self._checkexec:
136 def f(x):
137 def f(x):
137 p = self._join(x)
138 p = self._join(x)
138 if os.path.islink(p):
139 if os.path.islink(p):
139 return 'l'
140 return 'l'
140 if util.is_exec(p):
141 if util.is_exec(p):
141 return 'x'
142 return 'x'
142 return ''
143 return ''
143 return f
144 return f
144 def f(x):
145 def f(x):
145 if os.path.islink(self._join(x)):
146 if os.path.islink(self._join(x)):
146 return 'l'
147 return 'l'
147 if 'x' in fallback(x):
148 if 'x' in fallback(x):
148 return 'x'
149 return 'x'
149 return ''
150 return ''
150 return f
151 return f
151 if self._checkexec:
152 if self._checkexec:
152 def f(x):
153 def f(x):
153 if 'l' in fallback(x):
154 if 'l' in fallback(x):
154 return 'l'
155 return 'l'
155 if util.is_exec(self._join(x)):
156 if util.is_exec(self._join(x)):
156 return 'x'
157 return 'x'
157 return ''
158 return ''
158 return f
159 return f
159 return fallback
160 return fallback
160
161
161 def getcwd(self):
162 def getcwd(self):
162 cwd = os.getcwd()
163 cwd = os.getcwd()
163 if cwd == self._root:
164 if cwd == self._root:
164 return ''
165 return ''
165 # self._root ends with a path separator if self._root is '/' or 'C:\'
166 # self._root ends with a path separator if self._root is '/' or 'C:\'
166 rootsep = self._root
167 rootsep = self._root
167 if not util.endswithsep(rootsep):
168 if not util.endswithsep(rootsep):
168 rootsep += os.sep
169 rootsep += os.sep
169 if cwd.startswith(rootsep):
170 if cwd.startswith(rootsep):
170 return cwd[len(rootsep):]
171 return cwd[len(rootsep):]
171 else:
172 else:
172 # we're outside the repo. return an absolute path.
173 # we're outside the repo. return an absolute path.
173 return cwd
174 return cwd
174
175
175 def pathto(self, f, cwd=None):
176 def pathto(self, f, cwd=None):
176 if cwd is None:
177 if cwd is None:
177 cwd = self.getcwd()
178 cwd = self.getcwd()
178 path = util.pathto(self._root, cwd, f)
179 path = util.pathto(self._root, cwd, f)
179 if self._slash:
180 if self._slash:
180 return util.normpath(path)
181 return util.normpath(path)
181 return path
182 return path
182
183
183 def __getitem__(self, key):
184 def __getitem__(self, key):
184 '''Return the current state of key (a filename) in the dirstate.
185 '''Return the current state of key (a filename) in the dirstate.
185
186
186 States are:
187 States are:
187 n normal
188 n normal
188 m needs merging
189 m needs merging
189 r marked for removal
190 r marked for removal
190 a marked for addition
191 a marked for addition
191 ? not tracked
192 ? not tracked
192 '''
193 '''
193 return self._map.get(key, ("?",))[0]
194 return self._map.get(key, ("?",))[0]
194
195
195 def __contains__(self, key):
196 def __contains__(self, key):
196 return key in self._map
197 return key in self._map
197
198
198 def __iter__(self):
199 def __iter__(self):
199 for x in sorted(self._map):
200 for x in sorted(self._map):
200 yield x
201 yield x
201
202
202 def parents(self):
203 def parents(self):
203 return [self._validate(p) for p in self._pl]
204 return [self._validate(p) for p in self._pl]
204
205
205 def branch(self):
206 def branch(self):
206 return encoding.tolocal(self._branch)
207 return encoding.tolocal(self._branch)
207
208
208 def setparents(self, p1, p2=nullid):
209 def setparents(self, p1, p2=nullid):
209 self._dirty = self._dirtypl = True
210 self._dirty = self._dirtypl = True
210 self._pl = p1, p2
211 self._pl = p1, p2
211
212
212 def setbranch(self, branch):
213 def setbranch(self, branch):
213 if branch in ['tip', '.', 'null']:
214 if branch in ['tip', '.', 'null']:
214 raise util.Abort(_('the name \'%s\' is reserved') % branch)
215 raise util.Abort(_('the name \'%s\' is reserved') % branch)
215 self._branch = encoding.fromlocal(branch)
216 self._branch = encoding.fromlocal(branch)
216 self._opener("branch", "w").write(self._branch + '\n')
217 self._opener("branch", "w").write(self._branch + '\n')
217
218
218 def _read(self):
219 def _read(self):
219 self._map = {}
220 self._map = {}
220 self._copymap = {}
221 self._copymap = {}
221 try:
222 try:
222 st = self._opener("dirstate").read()
223 st = self._opener("dirstate").read()
223 except IOError, err:
224 except IOError, err:
224 if err.errno != errno.ENOENT:
225 if err.errno != errno.ENOENT:
225 raise
226 raise
226 return
227 return
227 if not st:
228 if not st:
228 return
229 return
229
230
230 p = parsers.parse_dirstate(self._map, self._copymap, st)
231 p = parsers.parse_dirstate(self._map, self._copymap, st)
231 if not self._dirtypl:
232 if not self._dirtypl:
232 self._pl = p
233 self._pl = p
233
234
234 def invalidate(self):
235 def invalidate(self):
235 for a in ("_map", "_copymap", "_foldmap", "_branch", "_pl", "_dirs",
236 for a in ("_map", "_copymap", "_foldmap", "_branch", "_pl", "_dirs",
236 "_ignore"):
237 "_ignore"):
237 if a in self.__dict__:
238 if a in self.__dict__:
238 delattr(self, a)
239 delattr(self, a)
239 self._dirty = False
240 self._dirty = False
240
241
241 def copy(self, source, dest):
242 def copy(self, source, dest):
242 """Mark dest as a copy of source. Unmark dest if source is None."""
243 """Mark dest as a copy of source. Unmark dest if source is None."""
243 if source == dest:
244 if source == dest:
244 return
245 return
245 self._dirty = True
246 self._dirty = True
246 if source is not None:
247 if source is not None:
247 self._copymap[dest] = source
248 self._copymap[dest] = source
248 elif dest in self._copymap:
249 elif dest in self._copymap:
249 del self._copymap[dest]
250 del self._copymap[dest]
250
251
251 def copied(self, file):
252 def copied(self, file):
252 return self._copymap.get(file, None)
253 return self._copymap.get(file, None)
253
254
254 def copies(self):
255 def copies(self):
255 return self._copymap
256 return self._copymap
256
257
257 def _droppath(self, f):
258 def _droppath(self, f):
258 if self[f] not in "?r" and "_dirs" in self.__dict__:
259 if self[f] not in "?r" and "_dirs" in self.__dict__:
259 _decdirs(self._dirs, f)
260 _decdirs(self._dirs, f)
260
261
261 def _addpath(self, f, check=False):
262 def _addpath(self, f, check=False):
262 oldstate = self[f]
263 oldstate = self[f]
263 if check or oldstate == "r":
264 if check or oldstate == "r":
264 if '\r' in f or '\n' in f:
265 if '\r' in f or '\n' in f:
265 raise util.Abort(
266 raise util.Abort(
266 _("'\\n' and '\\r' disallowed in filenames: %r") % f)
267 _("'\\n' and '\\r' disallowed in filenames: %r") % f)
267 if f in self._dirs:
268 if f in self._dirs:
268 raise util.Abort(_('directory %r already in dirstate') % f)
269 raise util.Abort(_('directory %r already in dirstate') % f)
269 # shadows
270 # shadows
270 for d in _finddirs(f):
271 for d in _finddirs(f):
271 if d in self._dirs:
272 if d in self._dirs:
272 break
273 break
273 if d in self._map and self[d] != 'r':
274 if d in self._map and self[d] != 'r':
274 raise util.Abort(
275 raise util.Abort(
275 _('file %r in dirstate clashes with %r') % (d, f))
276 _('file %r in dirstate clashes with %r') % (d, f))
276 if oldstate in "?r" and "_dirs" in self.__dict__:
277 if oldstate in "?r" and "_dirs" in self.__dict__:
277 _incdirs(self._dirs, f)
278 _incdirs(self._dirs, f)
278
279
279 def normal(self, f):
280 def normal(self, f):
280 '''Mark a file normal and clean.'''
281 '''Mark a file normal and clean.'''
281 self._dirty = True
282 self._dirty = True
282 self._addpath(f)
283 self._addpath(f)
283 s = os.lstat(self._join(f))
284 s = os.lstat(self._join(f))
284 self._map[f] = ('n', s.st_mode, s.st_size, int(s.st_mtime))
285 self._map[f] = ('n', s.st_mode, s.st_size, int(s.st_mtime))
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:
291 # if there is a merge going on and the file was either
298 # if there is a merge going on and the file was either
292 # in state 'm' (-1) or coming from other parent (-2) before
299 # in state 'm' (-1) or coming from other parent (-2) before
293 # being removed, restore that state.
300 # being removed, restore that state.
294 entry = self._map[f]
301 entry = self._map[f]
295 if entry[0] == 'r' and entry[2] in (-1, -2):
302 if entry[0] == 'r' and entry[2] in (-1, -2):
296 source = self._copymap.get(f)
303 source = self._copymap.get(f)
297 if entry[2] == -1:
304 if entry[2] == -1:
298 self.merge(f)
305 self.merge(f)
299 elif entry[2] == -2:
306 elif entry[2] == -2:
300 self.otherparent(f)
307 self.otherparent(f)
301 if source:
308 if source:
302 self.copy(source, f)
309 self.copy(source, f)
303 return
310 return
304 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
311 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
305 return
312 return
306 self._dirty = True
313 self._dirty = True
307 self._addpath(f)
314 self._addpath(f)
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.'''
314 if self._pl[1] == nullid:
322 if self._pl[1] == nullid:
315 raise util.Abort(_("setting %r to other parent "
323 raise util.Abort(_("setting %r to other parent "
316 "only allowed in merges") % f)
324 "only allowed in merges") % f)
317 self._dirty = True
325 self._dirty = True
318 self._addpath(f)
326 self._addpath(f)
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.'''
325 self._dirty = True
334 self._dirty = True
326 self._addpath(f, True)
335 self._addpath(f, True)
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.'''
333 self._dirty = True
343 self._dirty = True
334 self._droppath(f)
344 self._droppath(f)
335 size = 0
345 size = 0
336 if self._pl[1] != nullid and f in self._map:
346 if self._pl[1] != nullid and f in self._map:
337 # backup the previous state
347 # backup the previous state
338 entry = self._map[f]
348 entry = self._map[f]
339 if entry[0] == 'm': # merge
349 if entry[0] == 'm': # merge
340 size = -1
350 size = -1
341 elif entry[0] == 'n' and entry[2] == -2: # other parent
351 elif entry[0] == 'n' and entry[2] == -2: # other parent
342 size = -2
352 size = -2
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.'''
349 self._dirty = True
360 self._dirty = True
350 s = os.lstat(self._join(f))
361 s = os.lstat(self._join(f))
351 self._addpath(f)
362 self._addpath(f)
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.'''
358 self._dirty = True
370 self._dirty = True
359 try:
371 try:
360 self._droppath(f)
372 self._droppath(f)
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)
367 fold_path = self._foldmap.get(norm_path, None)
380 fold_path = self._foldmap.get(norm_path, None)
368 if fold_path is None:
381 if fold_path is None:
369 if knownpath or not os.path.lexists(os.path.join(self._root, path)):
382 if knownpath or not os.path.lexists(os.path.join(self._root, path)):
370 fold_path = path
383 fold_path = path
371 else:
384 else:
372 fold_path = self._foldmap.setdefault(norm_path,
385 fold_path = self._foldmap.setdefault(norm_path,
373 util.fspath(path, self._root))
386 util.fspath(path, self._root))
374 return fold_path
387 return fold_path
375
388
376 def clear(self):
389 def clear(self):
377 self._map = {}
390 self._map = {}
378 if "_dirs" in self.__dict__:
391 if "_dirs" in self.__dict__:
379 delattr(self, "_dirs")
392 delattr(self, "_dirs")
380 self._copymap = {}
393 self._copymap = {}
381 self._pl = [nullid, nullid]
394 self._pl = [nullid, nullid]
382 self._dirty = True
395 self._dirty = True
383
396
384 def rebuild(self, parent, files):
397 def rebuild(self, parent, files):
385 self.clear()
398 self.clear()
386 for f in files:
399 for f in files:
387 if 'x' in files.flags(f):
400 if 'x' in files.flags(f):
388 self._map[f] = ('n', 0777, -1, 0)
401 self._map[f] = ('n', 0777, -1, 0)
389 else:
402 else:
390 self._map[f] = ('n', 0666, -1, 0)
403 self._map[f] = ('n', 0666, -1, 0)
391 self._pl = (parent, nullid)
404 self._pl = (parent, nullid)
392 self._dirty = True
405 self._dirty = True
393
406
394 def write(self):
407 def write(self):
395 if not self._dirty:
408 if not self._dirty:
396 return
409 return
397 st = self._opener("dirstate", "w", atomictemp=True)
410 st = self._opener("dirstate", "w", atomictemp=True)
398
411
399 # use the modification time of the newly created temporary file as the
412 # use the modification time of the newly created temporary file as the
400 # filesystem's notion of 'now'
413 # filesystem's notion of 'now'
401 now = int(util.fstat(st).st_mtime)
414 now = int(util.fstat(st).st_mtime)
402
415
403 cs = cStringIO.StringIO()
416 cs = cStringIO.StringIO()
404 copymap = self._copymap
417 copymap = self._copymap
405 pack = struct.pack
418 pack = struct.pack
406 write = cs.write
419 write = cs.write
407 write("".join(self._pl))
420 write("".join(self._pl))
408 for f, e in self._map.iteritems():
421 for f, e in self._map.iteritems():
409 if e[0] == 'n' and e[3] == now:
422 if e[0] == 'n' and e[3] == now:
410 # The file was last modified "simultaneously" with the current
423 # The file was last modified "simultaneously" with the current
411 # write to dirstate (i.e. within the same second for file-
424 # write to dirstate (i.e. within the same second for file-
412 # systems with a granularity of 1 sec). This commonly happens
425 # systems with a granularity of 1 sec). This commonly happens
413 # for at least a couple of files on 'update'.
426 # for at least a couple of files on 'update'.
414 # The user could change the file without changing its size
427 # The user could change the file without changing its size
415 # within the same second. Invalidate the file's stat data in
428 # within the same second. Invalidate the file's stat data in
416 # dirstate, forcing future 'status' calls to compare the
429 # dirstate, forcing future 'status' calls to compare the
417 # contents of the file. This prevents mistakenly treating such
430 # contents of the file. This prevents mistakenly treating such
418 # files as clean.
431 # files as clean.
419 e = (e[0], 0, -1, -1) # mark entry as 'unset'
432 e = (e[0], 0, -1, -1) # mark entry as 'unset'
420 self._map[f] = e
433 self._map[f] = e
421
434
422 if f in copymap:
435 if f in copymap:
423 f = "%s\0%s" % (f, copymap[f])
436 f = "%s\0%s" % (f, copymap[f])
424 e = pack(_format, e[0], e[1], e[2], e[3], len(f))
437 e = pack(_format, e[0], e[1], e[2], e[3], len(f))
425 write(e)
438 write(e)
426 write(f)
439 write(f)
427 st.write(cs.getvalue())
440 st.write(cs.getvalue())
428 st.rename()
441 st.rename()
429 self._dirty = self._dirtypl = False
442 self._dirty = self._dirtypl = False
430
443
431 def _dirignore(self, f):
444 def _dirignore(self, f):
432 if f == '.':
445 if f == '.':
433 return False
446 return False
434 if self._ignore(f):
447 if self._ignore(f):
435 return True
448 return True
436 for p in _finddirs(f):
449 for p in _finddirs(f):
437 if self._ignore(p):
450 if self._ignore(p):
438 return True
451 return True
439 return False
452 return False
440
453
441 def walk(self, match, subrepos, unknown, ignored):
454 def walk(self, match, subrepos, unknown, ignored):
442 '''
455 '''
443 Walk recursively through the directory tree, finding all files
456 Walk recursively through the directory tree, finding all files
444 matched by match.
457 matched by match.
445
458
446 Return a dict mapping filename to stat-like object (either
459 Return a dict mapping filename to stat-like object (either
447 mercurial.osutil.stat instance or return value of os.stat()).
460 mercurial.osutil.stat instance or return value of os.stat()).
448 '''
461 '''
449
462
450 def fwarn(f, msg):
463 def fwarn(f, msg):
451 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
464 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
452 return False
465 return False
453
466
454 def badtype(mode):
467 def badtype(mode):
455 kind = _('unknown')
468 kind = _('unknown')
456 if stat.S_ISCHR(mode):
469 if stat.S_ISCHR(mode):
457 kind = _('character device')
470 kind = _('character device')
458 elif stat.S_ISBLK(mode):
471 elif stat.S_ISBLK(mode):
459 kind = _('block device')
472 kind = _('block device')
460 elif stat.S_ISFIFO(mode):
473 elif stat.S_ISFIFO(mode):
461 kind = _('fifo')
474 kind = _('fifo')
462 elif stat.S_ISSOCK(mode):
475 elif stat.S_ISSOCK(mode):
463 kind = _('socket')
476 kind = _('socket')
464 elif stat.S_ISDIR(mode):
477 elif stat.S_ISDIR(mode):
465 kind = _('directory')
478 kind = _('directory')
466 return _('unsupported file type (type is %s)') % kind
479 return _('unsupported file type (type is %s)') % kind
467
480
468 ignore = self._ignore
481 ignore = self._ignore
469 dirignore = self._dirignore
482 dirignore = self._dirignore
470 if ignored:
483 if ignored:
471 ignore = util.never
484 ignore = util.never
472 dirignore = util.never
485 dirignore = util.never
473 elif not unknown:
486 elif not unknown:
474 # if unknown and ignored are False, skip step 2
487 # if unknown and ignored are False, skip step 2
475 ignore = util.always
488 ignore = util.always
476 dirignore = util.always
489 dirignore = util.always
477
490
478 matchfn = match.matchfn
491 matchfn = match.matchfn
479 badfn = match.bad
492 badfn = match.bad
480 dmap = self._map
493 dmap = self._map
481 normpath = util.normpath
494 normpath = util.normpath
482 listdir = osutil.listdir
495 listdir = osutil.listdir
483 lstat = os.lstat
496 lstat = os.lstat
484 getkind = stat.S_IFMT
497 getkind = stat.S_IFMT
485 dirkind = stat.S_IFDIR
498 dirkind = stat.S_IFDIR
486 regkind = stat.S_IFREG
499 regkind = stat.S_IFREG
487 lnkkind = stat.S_IFLNK
500 lnkkind = stat.S_IFLNK
488 join = self._join
501 join = self._join
489 work = []
502 work = []
490 wadd = work.append
503 wadd = work.append
491
504
492 exact = skipstep3 = False
505 exact = skipstep3 = False
493 if matchfn == match.exact: # match.exact
506 if matchfn == match.exact: # match.exact
494 exact = True
507 exact = True
495 dirignore = util.always # skip step 2
508 dirignore = util.always # skip step 2
496 elif match.files() and not match.anypats(): # match.match, no patterns
509 elif match.files() and not match.anypats(): # match.match, no patterns
497 skipstep3 = True
510 skipstep3 = True
498
511
499 if self._checkcase:
512 if self._checkcase:
500 normalize = self._normalize
513 normalize = self._normalize
501 skipstep3 = False
514 skipstep3 = False
502 else:
515 else:
503 normalize = lambda x, y: x
516 normalize = lambda x, y: x
504
517
505 files = sorted(match.files())
518 files = sorted(match.files())
506 subrepos.sort()
519 subrepos.sort()
507 i, j = 0, 0
520 i, j = 0, 0
508 while i < len(files) and j < len(subrepos):
521 while i < len(files) and j < len(subrepos):
509 subpath = subrepos[j] + "/"
522 subpath = subrepos[j] + "/"
510 if files[i] < subpath:
523 if files[i] < subpath:
511 i += 1
524 i += 1
512 continue
525 continue
513 while i < len(files) and files[i].startswith(subpath):
526 while i < len(files) and files[i].startswith(subpath):
514 del files[i]
527 del files[i]
515 j += 1
528 j += 1
516
529
517 if not files or '.' in files:
530 if not files or '.' in files:
518 files = ['']
531 files = ['']
519 results = dict.fromkeys(subrepos)
532 results = dict.fromkeys(subrepos)
520 results['.hg'] = None
533 results['.hg'] = None
521
534
522 # step 1: find all explicit files
535 # step 1: find all explicit files
523 for ff in files:
536 for ff in files:
524 nf = normalize(normpath(ff), False)
537 nf = normalize(normpath(ff), False)
525 if nf in results:
538 if nf in results:
526 continue
539 continue
527
540
528 try:
541 try:
529 st = lstat(join(nf))
542 st = lstat(join(nf))
530 kind = getkind(st.st_mode)
543 kind = getkind(st.st_mode)
531 if kind == dirkind:
544 if kind == dirkind:
532 skipstep3 = False
545 skipstep3 = False
533 if nf in dmap:
546 if nf in dmap:
534 #file deleted on disk but still in dirstate
547 #file deleted on disk but still in dirstate
535 results[nf] = None
548 results[nf] = None
536 match.dir(nf)
549 match.dir(nf)
537 if not dirignore(nf):
550 if not dirignore(nf):
538 wadd(nf)
551 wadd(nf)
539 elif kind == regkind or kind == lnkkind:
552 elif kind == regkind or kind == lnkkind:
540 results[nf] = st
553 results[nf] = st
541 else:
554 else:
542 badfn(ff, badtype(kind))
555 badfn(ff, badtype(kind))
543 if nf in dmap:
556 if nf in dmap:
544 results[nf] = None
557 results[nf] = None
545 except OSError, inst:
558 except OSError, inst:
546 if nf in dmap: # does it exactly match a file?
559 if nf in dmap: # does it exactly match a file?
547 results[nf] = None
560 results[nf] = None
548 else: # does it match a directory?
561 else: # does it match a directory?
549 prefix = nf + "/"
562 prefix = nf + "/"
550 for fn in dmap:
563 for fn in dmap:
551 if fn.startswith(prefix):
564 if fn.startswith(prefix):
552 match.dir(nf)
565 match.dir(nf)
553 skipstep3 = False
566 skipstep3 = False
554 break
567 break
555 else:
568 else:
556 badfn(ff, inst.strerror)
569 badfn(ff, inst.strerror)
557
570
558 # step 2: visit subdirectories
571 # step 2: visit subdirectories
559 while work:
572 while work:
560 nd = work.pop()
573 nd = work.pop()
561 skip = None
574 skip = None
562 if nd == '.':
575 if nd == '.':
563 nd = ''
576 nd = ''
564 else:
577 else:
565 skip = '.hg'
578 skip = '.hg'
566 try:
579 try:
567 entries = listdir(join(nd), stat=True, skip=skip)
580 entries = listdir(join(nd), stat=True, skip=skip)
568 except OSError, inst:
581 except OSError, inst:
569 if inst.errno == errno.EACCES:
582 if inst.errno == errno.EACCES:
570 fwarn(nd, inst.strerror)
583 fwarn(nd, inst.strerror)
571 continue
584 continue
572 raise
585 raise
573 for f, kind, st in entries:
586 for f, kind, st in entries:
574 nf = normalize(nd and (nd + "/" + f) or f, True)
587 nf = normalize(nd and (nd + "/" + f) or f, True)
575 if nf not in results:
588 if nf not in results:
576 if kind == dirkind:
589 if kind == dirkind:
577 if not ignore(nf):
590 if not ignore(nf):
578 match.dir(nf)
591 match.dir(nf)
579 wadd(nf)
592 wadd(nf)
580 if nf in dmap and matchfn(nf):
593 if nf in dmap and matchfn(nf):
581 results[nf] = None
594 results[nf] = None
582 elif kind == regkind or kind == lnkkind:
595 elif kind == regkind or kind == lnkkind:
583 if nf in dmap:
596 if nf in dmap:
584 if matchfn(nf):
597 if matchfn(nf):
585 results[nf] = st
598 results[nf] = st
586 elif matchfn(nf) and not ignore(nf):
599 elif matchfn(nf) and not ignore(nf):
587 results[nf] = st
600 results[nf] = st
588 elif nf in dmap and matchfn(nf):
601 elif nf in dmap and matchfn(nf):
589 results[nf] = None
602 results[nf] = None
590
603
591 # step 3: report unseen items in the dmap hash
604 # step 3: report unseen items in the dmap hash
592 if not skipstep3 and not exact:
605 if not skipstep3 and not exact:
593 visit = sorted([f for f in dmap if f not in results and matchfn(f)])
606 visit = sorted([f for f in dmap if f not in results and matchfn(f)])
594 for nf, st in zip(visit, util.statfiles([join(i) for i in visit])):
607 for nf, st in zip(visit, util.statfiles([join(i) for i in visit])):
595 if not st is None and not getkind(st.st_mode) in (regkind, lnkkind):
608 if not st is None and not getkind(st.st_mode) in (regkind, lnkkind):
596 st = None
609 st = None
597 results[nf] = st
610 results[nf] = st
598 for s in subrepos:
611 for s in subrepos:
599 del results[s]
612 del results[s]
600 del results['.hg']
613 del results['.hg']
601 return results
614 return results
602
615
603 def status(self, match, subrepos, ignored, clean, unknown):
616 def status(self, match, subrepos, ignored, clean, unknown):
604 '''Determine the status of the working copy relative to the
617 '''Determine the status of the working copy relative to the
605 dirstate and return a tuple of lists (unsure, modified, added,
618 dirstate and return a tuple of lists (unsure, modified, added,
606 removed, deleted, unknown, ignored, clean), where:
619 removed, deleted, unknown, ignored, clean), where:
607
620
608 unsure:
621 unsure:
609 files that might have been modified since the dirstate was
622 files that might have been modified since the dirstate was
610 written, but need to be read to be sure (size is the same
623 written, but need to be read to be sure (size is the same
611 but mtime differs)
624 but mtime differs)
612 modified:
625 modified:
613 files that have definitely been modified since the dirstate
626 files that have definitely been modified since the dirstate
614 was written (different size or mode)
627 was written (different size or mode)
615 added:
628 added:
616 files that have been explicitly added with hg add
629 files that have been explicitly added with hg add
617 removed:
630 removed:
618 files that have been explicitly removed with hg remove
631 files that have been explicitly removed with hg remove
619 deleted:
632 deleted:
620 files that have been deleted through other means ("missing")
633 files that have been deleted through other means ("missing")
621 unknown:
634 unknown:
622 files not in the dirstate that are not ignored
635 files not in the dirstate that are not ignored
623 ignored:
636 ignored:
624 files not in the dirstate that are ignored
637 files not in the dirstate that are ignored
625 (by _dirignore())
638 (by _dirignore())
626 clean:
639 clean:
627 files that have definitely not been modified since the
640 files that have definitely not been modified since the
628 dirstate was written
641 dirstate was written
629 '''
642 '''
630 listignored, listclean, listunknown = ignored, clean, unknown
643 listignored, listclean, listunknown = ignored, clean, unknown
631 lookup, modified, added, unknown, ignored = [], [], [], [], []
644 lookup, modified, added, unknown, ignored = [], [], [], [], []
632 removed, deleted, clean = [], [], []
645 removed, deleted, clean = [], [], []
633
646
634 dmap = self._map
647 dmap = self._map
635 ladd = lookup.append # aka "unsure"
648 ladd = lookup.append # aka "unsure"
636 madd = modified.append
649 madd = modified.append
637 aadd = added.append
650 aadd = added.append
638 uadd = unknown.append
651 uadd = unknown.append
639 iadd = ignored.append
652 iadd = ignored.append
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
646 for fn, st in self.walk(match, subrepos, listunknown,
660 for fn, st in self.walk(match, subrepos, listunknown,
647 listignored).iteritems():
661 listignored).iteritems():
648 if fn not in dmap:
662 if fn not in dmap:
649 if (listignored or match.exact(fn)) and self._dirignore(fn):
663 if (listignored or match.exact(fn)) and self._dirignore(fn):
650 if listignored:
664 if listignored:
651 iadd(fn)
665 iadd(fn)
652 elif listunknown:
666 elif listunknown:
653 uadd(fn)
667 uadd(fn)
654 continue
668 continue
655
669
656 state, mode, size, time = dmap[fn]
670 state, mode, size, time = dmap[fn]
657
671
658 if not st and state in "nma":
672 if not st and state in "nma":
659 dadd(fn)
673 dadd(fn)
660 elif state == 'n':
674 elif state == 'n':
661 # The "mode & lnkkind != lnkkind or self._checklink"
675 # The "mode & lnkkind != lnkkind or self._checklink"
662 # lines are an expansion of "islink => checklink"
676 # lines are an expansion of "islink => checklink"
663 # where islink means "is this a link?" and checklink
677 # where islink means "is this a link?" and checklink
664 # means "can we check links?".
678 # means "can we check links?".
665 if (size >= 0 and
679 if (size >= 0 and
666 (size != st.st_size
680 (size != st.st_size
667 or ((mode ^ st.st_mode) & 0100 and self._checkexec))
681 or ((mode ^ st.st_mode) & 0100 and self._checkexec))
668 and (mode & lnkkind != lnkkind or self._checklink)
682 and (mode & lnkkind != lnkkind or self._checklink)
669 or size == -2 # other parent
683 or size == -2 # other parent
670 or fn in self._copymap):
684 or fn in self._copymap):
671 madd(fn)
685 madd(fn)
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':
678 madd(fn)
704 madd(fn)
679 elif state == 'a':
705 elif state == 'a':
680 aadd(fn)
706 aadd(fn)
681 elif state == 'r':
707 elif state == 'r':
682 radd(fn)
708 radd(fn)
683
709
684 return (lookup, modified, added, removed, deleted, unknown, ignored,
710 return (lookup, modified, added, removed, deleted, unknown, ignored,
685 clean)
711 clean)
General Comments 0
You need to be logged in to leave comments. Login now