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