##// END OF EJS Templates
dirstate: only call lstat once per flags invocation...
Bryan O'Sullivan -
r18869:e8b4b139 default
parent child Browse files
Show More
@@ -1,835 +1,838
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 import errno
8 8
9 9 from node import nullid
10 10 from i18n import _
11 11 import scmutil, util, ignore, osutil, parsers, encoding
12 12 import os, stat, errno, gc
13 13
14 14 propertycache = util.propertycache
15 15 filecache = scmutil.filecache
16 16 _rangemask = 0x7fffffff
17 17
18 18 class repocache(filecache):
19 19 """filecache for files in .hg/"""
20 20 def join(self, obj, fname):
21 21 return obj._opener.join(fname)
22 22
23 23 class rootcache(filecache):
24 24 """filecache for files in the repository root"""
25 25 def join(self, obj, fname):
26 26 return obj._join(fname)
27 27
28 28 def _finddirs(path):
29 29 pos = path.rfind('/')
30 30 while pos != -1:
31 31 yield path[:pos]
32 32 pos = path.rfind('/', 0, pos)
33 33
34 34 def _incdirs(dirs, path):
35 35 for base in _finddirs(path):
36 36 if base in dirs:
37 37 dirs[base] += 1
38 38 return
39 39 dirs[base] = 1
40 40
41 41 def _decdirs(dirs, path):
42 42 for base in _finddirs(path):
43 43 if dirs[base] > 1:
44 44 dirs[base] -= 1
45 45 return
46 46 del dirs[base]
47 47
48 48 class dirstate(object):
49 49
50 50 def __init__(self, opener, ui, root, validate):
51 51 '''Create a new dirstate object.
52 52
53 53 opener is an open()-like callable that can be used to open the
54 54 dirstate file; root is the root of the directory tracked by
55 55 the dirstate.
56 56 '''
57 57 self._opener = opener
58 58 self._validate = validate
59 59 self._root = root
60 60 self._rootdir = os.path.join(root, '')
61 61 self._dirty = False
62 62 self._dirtypl = False
63 63 self._lastnormaltime = 0
64 64 self._ui = ui
65 65 self._filecache = {}
66 66
67 67 @propertycache
68 68 def _map(self):
69 69 '''Return the dirstate contents as a map from filename to
70 70 (state, mode, size, time).'''
71 71 self._read()
72 72 return self._map
73 73
74 74 @propertycache
75 75 def _copymap(self):
76 76 self._read()
77 77 return self._copymap
78 78
79 79 @propertycache
80 80 def _foldmap(self):
81 81 f = {}
82 82 for name in self._map:
83 83 f[util.normcase(name)] = name
84 84 for name in self._dirs:
85 85 f[util.normcase(name)] = name
86 86 f['.'] = '.' # prevents useless util.fspath() invocation
87 87 return f
88 88
89 89 @repocache('branch')
90 90 def _branch(self):
91 91 try:
92 92 return self._opener.read("branch").strip() or "default"
93 93 except IOError, inst:
94 94 if inst.errno != errno.ENOENT:
95 95 raise
96 96 return "default"
97 97
98 98 @propertycache
99 99 def _pl(self):
100 100 try:
101 101 fp = self._opener("dirstate")
102 102 st = fp.read(40)
103 103 fp.close()
104 104 l = len(st)
105 105 if l == 40:
106 106 return st[:20], st[20:40]
107 107 elif l > 0 and l < 40:
108 108 raise util.Abort(_('working directory state appears damaged!'))
109 109 except IOError, err:
110 110 if err.errno != errno.ENOENT:
111 111 raise
112 112 return [nullid, nullid]
113 113
114 114 @propertycache
115 115 def _dirs(self):
116 116 dirs = {}
117 117 for f, s in self._map.iteritems():
118 118 if s[0] != 'r':
119 119 _incdirs(dirs, f)
120 120 return dirs
121 121
122 122 def dirs(self):
123 123 return self._dirs
124 124
125 125 @rootcache('.hgignore')
126 126 def _ignore(self):
127 127 files = [self._join('.hgignore')]
128 128 for name, path in self._ui.configitems("ui"):
129 129 if name == 'ignore' or name.startswith('ignore.'):
130 130 files.append(util.expandpath(path))
131 131 return ignore.ignore(self._root, files, self._ui.warn)
132 132
133 133 @propertycache
134 134 def _slash(self):
135 135 return self._ui.configbool('ui', 'slash') and os.sep != '/'
136 136
137 137 @propertycache
138 138 def _checklink(self):
139 139 return util.checklink(self._root)
140 140
141 141 @propertycache
142 142 def _checkexec(self):
143 143 return util.checkexec(self._root)
144 144
145 145 @propertycache
146 146 def _checkcase(self):
147 147 return not util.checkcase(self._join('.hg'))
148 148
149 149 def _join(self, f):
150 150 # much faster than os.path.join()
151 151 # it's safe because f is always a relative path
152 152 return self._rootdir + f
153 153
154 154 def flagfunc(self, buildfallback):
155 155 if self._checklink and self._checkexec:
156 156 def f(x):
157 p = self._join(x)
158 if os.path.islink(p):
159 return 'l'
160 if util.isexec(p):
161 return 'x'
157 try:
158 st = os.lstat(self._join(x))
159 if util.statislink(st):
160 return 'l'
161 if util.statisexec(st):
162 return 'x'
163 except OSError:
164 pass
162 165 return ''
163 166 return f
164 167
165 168 fallback = buildfallback()
166 169 if self._checklink:
167 170 def f(x):
168 171 if os.path.islink(self._join(x)):
169 172 return 'l'
170 173 if 'x' in fallback(x):
171 174 return 'x'
172 175 return ''
173 176 return f
174 177 if self._checkexec:
175 178 def f(x):
176 179 if 'l' in fallback(x):
177 180 return 'l'
178 181 if util.isexec(self._join(x)):
179 182 return 'x'
180 183 return ''
181 184 return f
182 185 else:
183 186 return fallback
184 187
185 188 def getcwd(self):
186 189 cwd = os.getcwd()
187 190 if cwd == self._root:
188 191 return ''
189 192 # self._root ends with a path separator if self._root is '/' or 'C:\'
190 193 rootsep = self._root
191 194 if not util.endswithsep(rootsep):
192 195 rootsep += os.sep
193 196 if cwd.startswith(rootsep):
194 197 return cwd[len(rootsep):]
195 198 else:
196 199 # we're outside the repo. return an absolute path.
197 200 return cwd
198 201
199 202 def pathto(self, f, cwd=None):
200 203 if cwd is None:
201 204 cwd = self.getcwd()
202 205 path = util.pathto(self._root, cwd, f)
203 206 if self._slash:
204 207 return util.normpath(path)
205 208 return path
206 209
207 210 def __getitem__(self, key):
208 211 '''Return the current state of key (a filename) in the dirstate.
209 212
210 213 States are:
211 214 n normal
212 215 m needs merging
213 216 r marked for removal
214 217 a marked for addition
215 218 ? not tracked
216 219 '''
217 220 return self._map.get(key, ("?",))[0]
218 221
219 222 def __contains__(self, key):
220 223 return key in self._map
221 224
222 225 def __iter__(self):
223 226 for x in sorted(self._map):
224 227 yield x
225 228
226 229 def iteritems(self):
227 230 return self._map.iteritems()
228 231
229 232 def parents(self):
230 233 return [self._validate(p) for p in self._pl]
231 234
232 235 def p1(self):
233 236 return self._validate(self._pl[0])
234 237
235 238 def p2(self):
236 239 return self._validate(self._pl[1])
237 240
238 241 def branch(self):
239 242 return encoding.tolocal(self._branch)
240 243
241 244 def setparents(self, p1, p2=nullid):
242 245 """Set dirstate parents to p1 and p2.
243 246
244 247 When moving from two parents to one, 'm' merged entries a
245 248 adjusted to normal and previous copy records discarded and
246 249 returned by the call.
247 250
248 251 See localrepo.setparents()
249 252 """
250 253 self._dirty = self._dirtypl = True
251 254 oldp2 = self._pl[1]
252 255 self._pl = p1, p2
253 256 copies = {}
254 257 if oldp2 != nullid and p2 == nullid:
255 258 # Discard 'm' markers when moving away from a merge state
256 259 for f, s in self._map.iteritems():
257 260 if s[0] == 'm':
258 261 if f in self._copymap:
259 262 copies[f] = self._copymap[f]
260 263 self.normallookup(f)
261 264 return copies
262 265
263 266 def setbranch(self, branch):
264 267 self._branch = encoding.fromlocal(branch)
265 268 f = self._opener('branch', 'w', atomictemp=True)
266 269 try:
267 270 f.write(self._branch + '\n')
268 271 f.close()
269 272
270 273 # make sure filecache has the correct stat info for _branch after
271 274 # replacing the underlying file
272 275 ce = self._filecache['_branch']
273 276 if ce:
274 277 ce.refresh()
275 278 except: # re-raises
276 279 f.discard()
277 280 raise
278 281
279 282 def _read(self):
280 283 self._map = {}
281 284 self._copymap = {}
282 285 try:
283 286 st = self._opener.read("dirstate")
284 287 except IOError, err:
285 288 if err.errno != errno.ENOENT:
286 289 raise
287 290 return
288 291 if not st:
289 292 return
290 293
291 294 # Python's garbage collector triggers a GC each time a certain number
292 295 # of container objects (the number being defined by
293 296 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
294 297 # for each file in the dirstate. The C version then immediately marks
295 298 # them as not to be tracked by the collector. However, this has no
296 299 # effect on when GCs are triggered, only on what objects the GC looks
297 300 # into. This means that O(number of files) GCs are unavoidable.
298 301 # Depending on when in the process's lifetime the dirstate is parsed,
299 302 # this can get very expensive. As a workaround, disable GC while
300 303 # parsing the dirstate.
301 304 gcenabled = gc.isenabled()
302 305 gc.disable()
303 306 try:
304 307 p = parsers.parse_dirstate(self._map, self._copymap, st)
305 308 finally:
306 309 if gcenabled:
307 310 gc.enable()
308 311 if not self._dirtypl:
309 312 self._pl = p
310 313
311 314 def invalidate(self):
312 315 for a in ("_map", "_copymap", "_foldmap", "_branch", "_pl", "_dirs",
313 316 "_ignore"):
314 317 if a in self.__dict__:
315 318 delattr(self, a)
316 319 self._lastnormaltime = 0
317 320 self._dirty = False
318 321
319 322 def copy(self, source, dest):
320 323 """Mark dest as a copy of source. Unmark dest if source is None."""
321 324 if source == dest:
322 325 return
323 326 self._dirty = True
324 327 if source is not None:
325 328 self._copymap[dest] = source
326 329 elif dest in self._copymap:
327 330 del self._copymap[dest]
328 331
329 332 def copied(self, file):
330 333 return self._copymap.get(file, None)
331 334
332 335 def copies(self):
333 336 return self._copymap
334 337
335 338 def _droppath(self, f):
336 339 if self[f] not in "?r" and "_dirs" in self.__dict__:
337 340 _decdirs(self._dirs, f)
338 341
339 342 def _addpath(self, f, state, mode, size, mtime):
340 343 oldstate = self[f]
341 344 if state == 'a' or oldstate == 'r':
342 345 scmutil.checkfilename(f)
343 346 if f in self._dirs:
344 347 raise util.Abort(_('directory %r already in dirstate') % f)
345 348 # shadows
346 349 for d in _finddirs(f):
347 350 if d in self._dirs:
348 351 break
349 352 if d in self._map and self[d] != 'r':
350 353 raise util.Abort(
351 354 _('file %r in dirstate clashes with %r') % (d, f))
352 355 if oldstate in "?r" and "_dirs" in self.__dict__:
353 356 _incdirs(self._dirs, f)
354 357 self._dirty = True
355 358 self._map[f] = (state, mode, size, mtime)
356 359
357 360 def normal(self, f):
358 361 '''Mark a file normal and clean.'''
359 362 s = os.lstat(self._join(f))
360 363 mtime = int(s.st_mtime)
361 364 self._addpath(f, 'n', s.st_mode,
362 365 s.st_size & _rangemask, mtime & _rangemask)
363 366 if f in self._copymap:
364 367 del self._copymap[f]
365 368 if mtime > self._lastnormaltime:
366 369 # Remember the most recent modification timeslot for status(),
367 370 # to make sure we won't miss future size-preserving file content
368 371 # modifications that happen within the same timeslot.
369 372 self._lastnormaltime = mtime
370 373
371 374 def normallookup(self, f):
372 375 '''Mark a file normal, but possibly dirty.'''
373 376 if self._pl[1] != nullid and f in self._map:
374 377 # if there is a merge going on and the file was either
375 378 # in state 'm' (-1) or coming from other parent (-2) before
376 379 # being removed, restore that state.
377 380 entry = self._map[f]
378 381 if entry[0] == 'r' and entry[2] in (-1, -2):
379 382 source = self._copymap.get(f)
380 383 if entry[2] == -1:
381 384 self.merge(f)
382 385 elif entry[2] == -2:
383 386 self.otherparent(f)
384 387 if source:
385 388 self.copy(source, f)
386 389 return
387 390 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
388 391 return
389 392 self._addpath(f, 'n', 0, -1, -1)
390 393 if f in self._copymap:
391 394 del self._copymap[f]
392 395
393 396 def otherparent(self, f):
394 397 '''Mark as coming from the other parent, always dirty.'''
395 398 if self._pl[1] == nullid:
396 399 raise util.Abort(_("setting %r to other parent "
397 400 "only allowed in merges") % f)
398 401 self._addpath(f, 'n', 0, -2, -1)
399 402 if f in self._copymap:
400 403 del self._copymap[f]
401 404
402 405 def add(self, f):
403 406 '''Mark a file added.'''
404 407 self._addpath(f, 'a', 0, -1, -1)
405 408 if f in self._copymap:
406 409 del self._copymap[f]
407 410
408 411 def remove(self, f):
409 412 '''Mark a file removed.'''
410 413 self._dirty = True
411 414 self._droppath(f)
412 415 size = 0
413 416 if self._pl[1] != nullid and f in self._map:
414 417 # backup the previous state
415 418 entry = self._map[f]
416 419 if entry[0] == 'm': # merge
417 420 size = -1
418 421 elif entry[0] == 'n' and entry[2] == -2: # other parent
419 422 size = -2
420 423 self._map[f] = ('r', 0, size, 0)
421 424 if size == 0 and f in self._copymap:
422 425 del self._copymap[f]
423 426
424 427 def merge(self, f):
425 428 '''Mark a file merged.'''
426 429 if self._pl[1] == nullid:
427 430 return self.normallookup(f)
428 431 s = os.lstat(self._join(f))
429 432 self._addpath(f, 'm', s.st_mode,
430 433 s.st_size & _rangemask, int(s.st_mtime) & _rangemask)
431 434 if f in self._copymap:
432 435 del self._copymap[f]
433 436
434 437 def drop(self, f):
435 438 '''Drop a file from the dirstate'''
436 439 if f in self._map:
437 440 self._dirty = True
438 441 self._droppath(f)
439 442 del self._map[f]
440 443
441 444 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
442 445 normed = util.normcase(path)
443 446 folded = self._foldmap.get(normed, None)
444 447 if folded is None:
445 448 if isknown:
446 449 folded = path
447 450 else:
448 451 if exists is None:
449 452 exists = os.path.lexists(os.path.join(self._root, path))
450 453 if not exists:
451 454 # Maybe a path component exists
452 455 if not ignoremissing and '/' in path:
453 456 d, f = path.rsplit('/', 1)
454 457 d = self._normalize(d, isknown, ignoremissing, None)
455 458 folded = d + "/" + f
456 459 else:
457 460 # No path components, preserve original case
458 461 folded = path
459 462 else:
460 463 # recursively normalize leading directory components
461 464 # against dirstate
462 465 if '/' in normed:
463 466 d, f = normed.rsplit('/', 1)
464 467 d = self._normalize(d, isknown, ignoremissing, True)
465 468 r = self._root + "/" + d
466 469 folded = d + "/" + util.fspath(f, r)
467 470 else:
468 471 folded = util.fspath(normed, self._root)
469 472 self._foldmap[normed] = folded
470 473
471 474 return folded
472 475
473 476 def normalize(self, path, isknown=False, ignoremissing=False):
474 477 '''
475 478 normalize the case of a pathname when on a casefolding filesystem
476 479
477 480 isknown specifies whether the filename came from walking the
478 481 disk, to avoid extra filesystem access.
479 482
480 483 If ignoremissing is True, missing path are returned
481 484 unchanged. Otherwise, we try harder to normalize possibly
482 485 existing path components.
483 486
484 487 The normalized case is determined based on the following precedence:
485 488
486 489 - version of name already stored in the dirstate
487 490 - version of name stored on disk
488 491 - version provided via command arguments
489 492 '''
490 493
491 494 if self._checkcase:
492 495 return self._normalize(path, isknown, ignoremissing)
493 496 return path
494 497
495 498 def clear(self):
496 499 self._map = {}
497 500 if "_dirs" in self.__dict__:
498 501 delattr(self, "_dirs")
499 502 self._copymap = {}
500 503 self._pl = [nullid, nullid]
501 504 self._lastnormaltime = 0
502 505 self._dirty = True
503 506
504 507 def rebuild(self, parent, allfiles, changedfiles=None):
505 508 changedfiles = changedfiles or allfiles
506 509 oldmap = self._map
507 510 self.clear()
508 511 for f in allfiles:
509 512 if f not in changedfiles:
510 513 self._map[f] = oldmap[f]
511 514 else:
512 515 if 'x' in allfiles.flags(f):
513 516 self._map[f] = ('n', 0777, -1, 0)
514 517 else:
515 518 self._map[f] = ('n', 0666, -1, 0)
516 519 self._pl = (parent, nullid)
517 520 self._dirty = True
518 521
519 522 def write(self):
520 523 if not self._dirty:
521 524 return
522 525 st = self._opener("dirstate", "w", atomictemp=True)
523 526
524 527 def finish(s):
525 528 st.write(s)
526 529 st.close()
527 530 self._lastnormaltime = 0
528 531 self._dirty = self._dirtypl = False
529 532
530 533 # use the modification time of the newly created temporary file as the
531 534 # filesystem's notion of 'now'
532 535 now = util.fstat(st).st_mtime
533 536 finish(parsers.pack_dirstate(self._map, self._copymap, self._pl, now))
534 537
535 538 def _dirignore(self, f):
536 539 if f == '.':
537 540 return False
538 541 if self._ignore(f):
539 542 return True
540 543 for p in _finddirs(f):
541 544 if self._ignore(p):
542 545 return True
543 546 return False
544 547
545 548 def walk(self, match, subrepos, unknown, ignored):
546 549 '''
547 550 Walk recursively through the directory tree, finding all files
548 551 matched by match.
549 552
550 553 Return a dict mapping filename to stat-like object (either
551 554 mercurial.osutil.stat instance or return value of os.stat()).
552 555 '''
553 556
554 557 def fwarn(f, msg):
555 558 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
556 559 return False
557 560
558 561 def badtype(mode):
559 562 kind = _('unknown')
560 563 if stat.S_ISCHR(mode):
561 564 kind = _('character device')
562 565 elif stat.S_ISBLK(mode):
563 566 kind = _('block device')
564 567 elif stat.S_ISFIFO(mode):
565 568 kind = _('fifo')
566 569 elif stat.S_ISSOCK(mode):
567 570 kind = _('socket')
568 571 elif stat.S_ISDIR(mode):
569 572 kind = _('directory')
570 573 return _('unsupported file type (type is %s)') % kind
571 574
572 575 ignore = self._ignore
573 576 dirignore = self._dirignore
574 577 if ignored:
575 578 ignore = util.never
576 579 dirignore = util.never
577 580 elif not unknown:
578 581 # if unknown and ignored are False, skip step 2
579 582 ignore = util.always
580 583 dirignore = util.always
581 584
582 585 matchfn = match.matchfn
583 586 matchalways = match.always()
584 587 badfn = match.bad
585 588 dmap = self._map
586 589 normpath = util.normpath
587 590 listdir = osutil.listdir
588 591 lstat = os.lstat
589 592 getkind = stat.S_IFMT
590 593 dirkind = stat.S_IFDIR
591 594 regkind = stat.S_IFREG
592 595 lnkkind = stat.S_IFLNK
593 596 join = self._join
594 597 work = []
595 598 wadd = work.append
596 599
597 600 exact = skipstep3 = False
598 601 if matchfn == match.exact: # match.exact
599 602 exact = True
600 603 dirignore = util.always # skip step 2
601 604 elif match.files() and not match.anypats(): # match.match, no patterns
602 605 skipstep3 = True
603 606
604 607 if not exact and self._checkcase:
605 608 normalize = self._normalize
606 609 skipstep3 = False
607 610 else:
608 611 normalize = None
609 612
610 613 files = sorted(match.files())
611 614 subrepos.sort()
612 615 i, j = 0, 0
613 616 while i < len(files) and j < len(subrepos):
614 617 subpath = subrepos[j] + "/"
615 618 if files[i] < subpath:
616 619 i += 1
617 620 continue
618 621 while i < len(files) and files[i].startswith(subpath):
619 622 del files[i]
620 623 j += 1
621 624
622 625 if not files or '.' in files:
623 626 files = ['']
624 627 results = dict.fromkeys(subrepos)
625 628 results['.hg'] = None
626 629
627 630 # step 1: find all explicit files
628 631 for ff in files:
629 632 if normalize:
630 633 nf = normalize(normpath(ff), False, True)
631 634 else:
632 635 nf = normpath(ff)
633 636 if nf in results:
634 637 continue
635 638
636 639 try:
637 640 st = lstat(join(nf))
638 641 kind = getkind(st.st_mode)
639 642 if kind == dirkind:
640 643 skipstep3 = False
641 644 if nf in dmap:
642 645 #file deleted on disk but still in dirstate
643 646 results[nf] = None
644 647 match.dir(nf)
645 648 if not dirignore(nf):
646 649 wadd(nf)
647 650 elif kind == regkind or kind == lnkkind:
648 651 results[nf] = st
649 652 else:
650 653 badfn(ff, badtype(kind))
651 654 if nf in dmap:
652 655 results[nf] = None
653 656 except OSError, inst:
654 657 if nf in dmap: # does it exactly match a file?
655 658 results[nf] = None
656 659 else: # does it match a directory?
657 660 prefix = nf + "/"
658 661 for fn in dmap:
659 662 if fn.startswith(prefix):
660 663 match.dir(nf)
661 664 skipstep3 = False
662 665 break
663 666 else:
664 667 badfn(ff, inst.strerror)
665 668
666 669 # step 2: visit subdirectories
667 670 while work:
668 671 nd = work.pop()
669 672 skip = None
670 673 if nd == '.':
671 674 nd = ''
672 675 else:
673 676 skip = '.hg'
674 677 try:
675 678 entries = listdir(join(nd), stat=True, skip=skip)
676 679 except OSError, inst:
677 680 if inst.errno in (errno.EACCES, errno.ENOENT):
678 681 fwarn(nd, inst.strerror)
679 682 continue
680 683 raise
681 684 for f, kind, st in entries:
682 685 if normalize:
683 686 nf = normalize(nd and (nd + "/" + f) or f, True, True)
684 687 else:
685 688 nf = nd and (nd + "/" + f) or f
686 689 if nf not in results:
687 690 if kind == dirkind:
688 691 if not ignore(nf):
689 692 match.dir(nf)
690 693 wadd(nf)
691 694 if nf in dmap and (matchalways or matchfn(nf)):
692 695 results[nf] = None
693 696 elif kind == regkind or kind == lnkkind:
694 697 if nf in dmap:
695 698 if matchalways or matchfn(nf):
696 699 results[nf] = st
697 700 elif (matchalways or matchfn(nf)) and not ignore(nf):
698 701 results[nf] = st
699 702 elif nf in dmap and (matchalways or matchfn(nf)):
700 703 results[nf] = None
701 704
702 705 for s in subrepos:
703 706 del results[s]
704 707 del results['.hg']
705 708
706 709 # step 3: report unseen items in the dmap hash
707 710 if not skipstep3 and not exact:
708 711 if not results and matchalways:
709 712 visit = dmap.keys()
710 713 else:
711 714 visit = [f for f in dmap if f not in results and matchfn(f)]
712 715 visit.sort()
713 716
714 717 if unknown:
715 718 # unknown == True means we walked the full directory tree above.
716 719 # So if a file is not seen it was either a) not matching matchfn
717 720 # b) ignored, c) missing, or d) under a symlink directory.
718 721 audit_path = scmutil.pathauditor(self._root)
719 722
720 723 for nf in iter(visit):
721 724 # Report ignored items in the dmap as long as they are not
722 725 # under a symlink directory.
723 726 if ignore(nf) and audit_path.check(nf):
724 727 try:
725 728 results[nf] = lstat(join(nf))
726 729 except OSError:
727 730 # file doesn't exist
728 731 results[nf] = None
729 732 else:
730 733 # It's either missing or under a symlink directory
731 734 results[nf] = None
732 735 else:
733 736 # We may not have walked the full directory tree above,
734 737 # so stat everything we missed.
735 738 nf = iter(visit).next
736 739 for st in util.statfiles([join(i) for i in visit]):
737 740 results[nf()] = st
738 741 return results
739 742
740 743 def status(self, match, subrepos, ignored, clean, unknown):
741 744 '''Determine the status of the working copy relative to the
742 745 dirstate and return a tuple of lists (unsure, modified, added,
743 746 removed, deleted, unknown, ignored, clean), where:
744 747
745 748 unsure:
746 749 files that might have been modified since the dirstate was
747 750 written, but need to be read to be sure (size is the same
748 751 but mtime differs)
749 752 modified:
750 753 files that have definitely been modified since the dirstate
751 754 was written (different size or mode)
752 755 added:
753 756 files that have been explicitly added with hg add
754 757 removed:
755 758 files that have been explicitly removed with hg remove
756 759 deleted:
757 760 files that have been deleted through other means ("missing")
758 761 unknown:
759 762 files not in the dirstate that are not ignored
760 763 ignored:
761 764 files not in the dirstate that are ignored
762 765 (by _dirignore())
763 766 clean:
764 767 files that have definitely not been modified since the
765 768 dirstate was written
766 769 '''
767 770 listignored, listclean, listunknown = ignored, clean, unknown
768 771 lookup, modified, added, unknown, ignored = [], [], [], [], []
769 772 removed, deleted, clean = [], [], []
770 773
771 774 dmap = self._map
772 775 ladd = lookup.append # aka "unsure"
773 776 madd = modified.append
774 777 aadd = added.append
775 778 uadd = unknown.append
776 779 iadd = ignored.append
777 780 radd = removed.append
778 781 dadd = deleted.append
779 782 cadd = clean.append
780 783 mexact = match.exact
781 784 dirignore = self._dirignore
782 785 checkexec = self._checkexec
783 786 checklink = self._checklink
784 787 copymap = self._copymap
785 788 lastnormaltime = self._lastnormaltime
786 789
787 790 lnkkind = stat.S_IFLNK
788 791
789 792 for fn, st in self.walk(match, subrepos, listunknown,
790 793 listignored).iteritems():
791 794 if fn not in dmap:
792 795 if (listignored or mexact(fn)) and dirignore(fn):
793 796 if listignored:
794 797 iadd(fn)
795 798 elif listunknown:
796 799 uadd(fn)
797 800 continue
798 801
799 802 state, mode, size, time = dmap[fn]
800 803
801 804 if not st and state in "nma":
802 805 dadd(fn)
803 806 elif state == 'n':
804 807 # The "mode & lnkkind != lnkkind or self._checklink"
805 808 # lines are an expansion of "islink => checklink"
806 809 # where islink means "is this a link?" and checklink
807 810 # means "can we check links?".
808 811 mtime = int(st.st_mtime)
809 812 if (size >= 0 and
810 813 ((size != st.st_size and size != st.st_size & _rangemask)
811 814 or ((mode ^ st.st_mode) & 0100 and checkexec))
812 815 and (mode & lnkkind != lnkkind or checklink)
813 816 or size == -2 # other parent
814 817 or fn in copymap):
815 818 madd(fn)
816 819 elif ((time != mtime and time != mtime & _rangemask)
817 820 and (mode & lnkkind != lnkkind or checklink)):
818 821 ladd(fn)
819 822 elif mtime == lastnormaltime:
820 823 # fn may have been changed in the same timeslot without
821 824 # changing its size. This can happen if we quickly do
822 825 # multiple commits in a single transaction.
823 826 # Force lookup, so we don't miss such a racy file change.
824 827 ladd(fn)
825 828 elif listclean:
826 829 cadd(fn)
827 830 elif state == 'm':
828 831 madd(fn)
829 832 elif state == 'a':
830 833 aadd(fn)
831 834 elif state == 'r':
832 835 radd(fn)
833 836
834 837 return (lookup, modified, added, removed, deleted, unknown, ignored,
835 838 clean)
General Comments 0
You need to be logged in to leave comments. Login now