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