##// END OF EJS Templates
dirstate: track updated files to improve write time...
Durham Goode -
r31206:49e5491e default
parent child Browse files
Show More
@@ -1,1245 +1,1256 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 __future__ import absolute_import
9 9
10 10 import collections
11 11 import errno
12 12 import os
13 13 import stat
14 14
15 15 from .i18n import _
16 16 from .node import nullid
17 17 from . import (
18 18 encoding,
19 19 error,
20 20 match as matchmod,
21 21 osutil,
22 22 parsers,
23 23 pathutil,
24 24 pycompat,
25 25 scmutil,
26 26 txnutil,
27 27 util,
28 28 )
29 29
30 30 propertycache = util.propertycache
31 31 filecache = scmutil.filecache
32 32 _rangemask = 0x7fffffff
33 33
34 34 dirstatetuple = parsers.dirstatetuple
35 35
36 36 class repocache(filecache):
37 37 """filecache for files in .hg/"""
38 38 def join(self, obj, fname):
39 39 return obj._opener.join(fname)
40 40
41 41 class rootcache(filecache):
42 42 """filecache for files in the repository root"""
43 43 def join(self, obj, fname):
44 44 return obj._join(fname)
45 45
46 46 def _getfsnow(vfs):
47 47 '''Get "now" timestamp on filesystem'''
48 48 tmpfd, tmpname = vfs.mkstemp()
49 49 try:
50 50 return os.fstat(tmpfd).st_mtime
51 51 finally:
52 52 os.close(tmpfd)
53 53 vfs.unlink(tmpname)
54 54
55 55 def nonnormalentries(dmap):
56 56 '''Compute the nonnormal dirstate entries from the dmap'''
57 57 try:
58 58 return parsers.nonnormalentries(dmap)
59 59 except AttributeError:
60 60 return set(fname for fname, e in dmap.iteritems()
61 61 if e[0] != 'n' or e[3] == -1)
62 62
63 63 class dirstate(object):
64 64
65 65 def __init__(self, opener, ui, root, validate):
66 66 '''Create a new dirstate object.
67 67
68 68 opener is an open()-like callable that can be used to open the
69 69 dirstate file; root is the root of the directory tracked by
70 70 the dirstate.
71 71 '''
72 72 self._opener = opener
73 73 self._validate = validate
74 74 self._root = root
75 75 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
76 76 # UNC path pointing to root share (issue4557)
77 77 self._rootdir = pathutil.normasprefix(root)
78 78 # internal config: ui.forcecwd
79 79 forcecwd = ui.config('ui', 'forcecwd')
80 80 if forcecwd:
81 81 self._cwd = forcecwd
82 82 self._dirty = False
83 83 self._dirtypl = False
84 84 self._lastnormaltime = 0
85 85 self._ui = ui
86 86 self._filecache = {}
87 87 self._parentwriters = 0
88 88 self._filename = 'dirstate'
89 89 self._pendingfilename = '%s.pending' % self._filename
90 90 self._plchangecallbacks = {}
91 91 self._origpl = None
92 self._updatedfiles = set()
92 93
93 94 # for consistent view between _pl() and _read() invocations
94 95 self._pendingmode = None
95 96
96 97 def beginparentchange(self):
97 98 '''Marks the beginning of a set of changes that involve changing
98 99 the dirstate parents. If there is an exception during this time,
99 100 the dirstate will not be written when the wlock is released. This
100 101 prevents writing an incoherent dirstate where the parent doesn't
101 102 match the contents.
102 103 '''
103 104 self._parentwriters += 1
104 105
105 106 def endparentchange(self):
106 107 '''Marks the end of a set of changes that involve changing the
107 108 dirstate parents. Once all parent changes have been marked done,
108 109 the wlock will be free to write the dirstate on release.
109 110 '''
110 111 if self._parentwriters > 0:
111 112 self._parentwriters -= 1
112 113
113 114 def pendingparentchange(self):
114 115 '''Returns true if the dirstate is in the middle of a set of changes
115 116 that modify the dirstate parent.
116 117 '''
117 118 return self._parentwriters > 0
118 119
119 120 @propertycache
120 121 def _map(self):
121 122 '''Return the dirstate contents as a map from filename to
122 123 (state, mode, size, time).'''
123 124 self._read()
124 125 return self._map
125 126
126 127 @propertycache
127 128 def _copymap(self):
128 129 self._read()
129 130 return self._copymap
130 131
131 132 @propertycache
132 133 def _nonnormalset(self):
133 134 return nonnormalentries(self._map)
134 135
135 136 @propertycache
136 137 def _filefoldmap(self):
137 138 try:
138 139 makefilefoldmap = parsers.make_file_foldmap
139 140 except AttributeError:
140 141 pass
141 142 else:
142 143 return makefilefoldmap(self._map, util.normcasespec,
143 144 util.normcasefallback)
144 145
145 146 f = {}
146 147 normcase = util.normcase
147 148 for name, s in self._map.iteritems():
148 149 if s[0] != 'r':
149 150 f[normcase(name)] = name
150 151 f['.'] = '.' # prevents useless util.fspath() invocation
151 152 return f
152 153
153 154 @propertycache
154 155 def _dirfoldmap(self):
155 156 f = {}
156 157 normcase = util.normcase
157 158 for name in self._dirs:
158 159 f[normcase(name)] = name
159 160 return f
160 161
161 162 @repocache('branch')
162 163 def _branch(self):
163 164 try:
164 165 return self._opener.read("branch").strip() or "default"
165 166 except IOError as inst:
166 167 if inst.errno != errno.ENOENT:
167 168 raise
168 169 return "default"
169 170
170 171 @propertycache
171 172 def _pl(self):
172 173 try:
173 174 fp = self._opendirstatefile()
174 175 st = fp.read(40)
175 176 fp.close()
176 177 l = len(st)
177 178 if l == 40:
178 179 return st[:20], st[20:40]
179 180 elif l > 0 and l < 40:
180 181 raise error.Abort(_('working directory state appears damaged!'))
181 182 except IOError as err:
182 183 if err.errno != errno.ENOENT:
183 184 raise
184 185 return [nullid, nullid]
185 186
186 187 @propertycache
187 188 def _dirs(self):
188 189 return util.dirs(self._map, 'r')
189 190
190 191 def dirs(self):
191 192 return self._dirs
192 193
193 194 @rootcache('.hgignore')
194 195 def _ignore(self):
195 196 files = self._ignorefiles()
196 197 if not files:
197 198 return util.never
198 199
199 200 pats = ['include:%s' % f for f in files]
200 201 return matchmod.match(self._root, '', [], pats, warn=self._ui.warn)
201 202
202 203 @propertycache
203 204 def _slash(self):
204 205 return self._ui.configbool('ui', 'slash') and pycompat.ossep != '/'
205 206
206 207 @propertycache
207 208 def _checklink(self):
208 209 return util.checklink(self._root)
209 210
210 211 @propertycache
211 212 def _checkexec(self):
212 213 return util.checkexec(self._root)
213 214
214 215 @propertycache
215 216 def _checkcase(self):
216 217 return not util.fscasesensitive(self._join('.hg'))
217 218
218 219 def _join(self, f):
219 220 # much faster than os.path.join()
220 221 # it's safe because f is always a relative path
221 222 return self._rootdir + f
222 223
223 224 def flagfunc(self, buildfallback):
224 225 if self._checklink and self._checkexec:
225 226 def f(x):
226 227 try:
227 228 st = os.lstat(self._join(x))
228 229 if util.statislink(st):
229 230 return 'l'
230 231 if util.statisexec(st):
231 232 return 'x'
232 233 except OSError:
233 234 pass
234 235 return ''
235 236 return f
236 237
237 238 fallback = buildfallback()
238 239 if self._checklink:
239 240 def f(x):
240 241 if os.path.islink(self._join(x)):
241 242 return 'l'
242 243 if 'x' in fallback(x):
243 244 return 'x'
244 245 return ''
245 246 return f
246 247 if self._checkexec:
247 248 def f(x):
248 249 if 'l' in fallback(x):
249 250 return 'l'
250 251 if util.isexec(self._join(x)):
251 252 return 'x'
252 253 return ''
253 254 return f
254 255 else:
255 256 return fallback
256 257
257 258 @propertycache
258 259 def _cwd(self):
259 260 return pycompat.getcwd()
260 261
261 262 def getcwd(self):
262 263 '''Return the path from which a canonical path is calculated.
263 264
264 265 This path should be used to resolve file patterns or to convert
265 266 canonical paths back to file paths for display. It shouldn't be
266 267 used to get real file paths. Use vfs functions instead.
267 268 '''
268 269 cwd = self._cwd
269 270 if cwd == self._root:
270 271 return ''
271 272 # self._root ends with a path separator if self._root is '/' or 'C:\'
272 273 rootsep = self._root
273 274 if not util.endswithsep(rootsep):
274 275 rootsep += pycompat.ossep
275 276 if cwd.startswith(rootsep):
276 277 return cwd[len(rootsep):]
277 278 else:
278 279 # we're outside the repo. return an absolute path.
279 280 return cwd
280 281
281 282 def pathto(self, f, cwd=None):
282 283 if cwd is None:
283 284 cwd = self.getcwd()
284 285 path = util.pathto(self._root, cwd, f)
285 286 if self._slash:
286 287 return util.pconvert(path)
287 288 return path
288 289
289 290 def __getitem__(self, key):
290 291 '''Return the current state of key (a filename) in the dirstate.
291 292
292 293 States are:
293 294 n normal
294 295 m needs merging
295 296 r marked for removal
296 297 a marked for addition
297 298 ? not tracked
298 299 '''
299 300 return self._map.get(key, ("?",))[0]
300 301
301 302 def __contains__(self, key):
302 303 return key in self._map
303 304
304 305 def __iter__(self):
305 306 for x in sorted(self._map):
306 307 yield x
307 308
308 309 def iteritems(self):
309 310 return self._map.iteritems()
310 311
311 312 def parents(self):
312 313 return [self._validate(p) for p in self._pl]
313 314
314 315 def p1(self):
315 316 return self._validate(self._pl[0])
316 317
317 318 def p2(self):
318 319 return self._validate(self._pl[1])
319 320
320 321 def branch(self):
321 322 return encoding.tolocal(self._branch)
322 323
323 324 def setparents(self, p1, p2=nullid):
324 325 """Set dirstate parents to p1 and p2.
325 326
326 327 When moving from two parents to one, 'm' merged entries a
327 328 adjusted to normal and previous copy records discarded and
328 329 returned by the call.
329 330
330 331 See localrepo.setparents()
331 332 """
332 333 if self._parentwriters == 0:
333 334 raise ValueError("cannot set dirstate parent without "
334 335 "calling dirstate.beginparentchange")
335 336
336 337 self._dirty = self._dirtypl = True
337 338 oldp2 = self._pl[1]
338 339 if self._origpl is None:
339 340 self._origpl = self._pl
340 341 self._pl = p1, p2
341 342 copies = {}
342 343 if oldp2 != nullid and p2 == nullid:
343 344 for f, s in self._map.iteritems():
344 345 # Discard 'm' markers when moving away from a merge state
345 346 if s[0] == 'm':
346 347 if f in self._copymap:
347 348 copies[f] = self._copymap[f]
348 349 self.normallookup(f)
349 350 # Also fix up otherparent markers
350 351 elif s[0] == 'n' and s[2] == -2:
351 352 if f in self._copymap:
352 353 copies[f] = self._copymap[f]
353 354 self.add(f)
354 355 return copies
355 356
356 357 def setbranch(self, branch):
357 358 self._branch = encoding.fromlocal(branch)
358 359 f = self._opener('branch', 'w', atomictemp=True, checkambig=True)
359 360 try:
360 361 f.write(self._branch + '\n')
361 362 f.close()
362 363
363 364 # make sure filecache has the correct stat info for _branch after
364 365 # replacing the underlying file
365 366 ce = self._filecache['_branch']
366 367 if ce:
367 368 ce.refresh()
368 369 except: # re-raises
369 370 f.discard()
370 371 raise
371 372
372 373 def _opendirstatefile(self):
373 374 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
374 375 if self._pendingmode is not None and self._pendingmode != mode:
375 376 fp.close()
376 377 raise error.Abort(_('working directory state may be '
377 378 'changed parallelly'))
378 379 self._pendingmode = mode
379 380 return fp
380 381
381 382 def _read(self):
382 383 self._map = {}
383 384 self._copymap = {}
384 385 try:
385 386 fp = self._opendirstatefile()
386 387 try:
387 388 st = fp.read()
388 389 finally:
389 390 fp.close()
390 391 except IOError as err:
391 392 if err.errno != errno.ENOENT:
392 393 raise
393 394 return
394 395 if not st:
395 396 return
396 397
397 398 if util.safehasattr(parsers, 'dict_new_presized'):
398 399 # Make an estimate of the number of files in the dirstate based on
399 400 # its size. From a linear regression on a set of real-world repos,
400 401 # all over 10,000 files, the size of a dirstate entry is 85
401 402 # bytes. The cost of resizing is significantly higher than the cost
402 403 # of filling in a larger presized dict, so subtract 20% from the
403 404 # size.
404 405 #
405 406 # This heuristic is imperfect in many ways, so in a future dirstate
406 407 # format update it makes sense to just record the number of entries
407 408 # on write.
408 409 self._map = parsers.dict_new_presized(len(st) / 71)
409 410
410 411 # Python's garbage collector triggers a GC each time a certain number
411 412 # of container objects (the number being defined by
412 413 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
413 414 # for each file in the dirstate. The C version then immediately marks
414 415 # them as not to be tracked by the collector. However, this has no
415 416 # effect on when GCs are triggered, only on what objects the GC looks
416 417 # into. This means that O(number of files) GCs are unavoidable.
417 418 # Depending on when in the process's lifetime the dirstate is parsed,
418 419 # this can get very expensive. As a workaround, disable GC while
419 420 # parsing the dirstate.
420 421 #
421 422 # (we cannot decorate the function directly since it is in a C module)
422 423 parse_dirstate = util.nogc(parsers.parse_dirstate)
423 424 p = parse_dirstate(self._map, self._copymap, st)
424 425 if not self._dirtypl:
425 426 self._pl = p
426 427
427 428 def invalidate(self):
428 429 for a in ("_map", "_copymap", "_filefoldmap", "_dirfoldmap", "_branch",
429 430 "_pl", "_dirs", "_ignore", "_nonnormalset"):
430 431 if a in self.__dict__:
431 432 delattr(self, a)
432 433 self._lastnormaltime = 0
433 434 self._dirty = False
435 self._updatedfiles.clear()
434 436 self._parentwriters = 0
435 437 self._origpl = None
436 438
437 439 def copy(self, source, dest):
438 440 """Mark dest as a copy of source. Unmark dest if source is None."""
439 441 if source == dest:
440 442 return
441 443 self._dirty = True
442 444 if source is not None:
443 445 self._copymap[dest] = source
446 self._updatedfiles.add(source)
447 self._updatedfiles.add(dest)
444 448 elif dest in self._copymap:
445 449 del self._copymap[dest]
450 self._updatedfiles.add(dest)
446 451
447 452 def copied(self, file):
448 453 return self._copymap.get(file, None)
449 454
450 455 def copies(self):
451 456 return self._copymap
452 457
453 458 def _droppath(self, f):
454 459 if self[f] not in "?r" and "_dirs" in self.__dict__:
455 460 self._dirs.delpath(f)
456 461
457 462 if "_filefoldmap" in self.__dict__:
458 463 normed = util.normcase(f)
459 464 if normed in self._filefoldmap:
460 465 del self._filefoldmap[normed]
461 466
467 self._updatedfiles.add(f)
468
462 469 def _addpath(self, f, state, mode, size, mtime):
463 470 oldstate = self[f]
464 471 if state == 'a' or oldstate == 'r':
465 472 scmutil.checkfilename(f)
466 473 if f in self._dirs:
467 474 raise error.Abort(_('directory %r already in dirstate') % f)
468 475 # shadows
469 476 for d in util.finddirs(f):
470 477 if d in self._dirs:
471 478 break
472 479 if d in self._map and self[d] != 'r':
473 480 raise error.Abort(
474 481 _('file %r in dirstate clashes with %r') % (d, f))
475 482 if oldstate in "?r" and "_dirs" in self.__dict__:
476 483 self._dirs.addpath(f)
477 484 self._dirty = True
485 self._updatedfiles.add(f)
478 486 self._map[f] = dirstatetuple(state, mode, size, mtime)
479 487 if state != 'n' or mtime == -1:
480 488 self._nonnormalset.add(f)
481 489
482 490 def normal(self, f):
483 491 '''Mark a file normal and clean.'''
484 492 s = os.lstat(self._join(f))
485 493 mtime = s.st_mtime
486 494 self._addpath(f, 'n', s.st_mode,
487 495 s.st_size & _rangemask, mtime & _rangemask)
488 496 if f in self._copymap:
489 497 del self._copymap[f]
490 498 if f in self._nonnormalset:
491 499 self._nonnormalset.remove(f)
492 500 if mtime > self._lastnormaltime:
493 501 # Remember the most recent modification timeslot for status(),
494 502 # to make sure we won't miss future size-preserving file content
495 503 # modifications that happen within the same timeslot.
496 504 self._lastnormaltime = mtime
497 505
498 506 def normallookup(self, f):
499 507 '''Mark a file normal, but possibly dirty.'''
500 508 if self._pl[1] != nullid and f in self._map:
501 509 # if there is a merge going on and the file was either
502 510 # in state 'm' (-1) or coming from other parent (-2) before
503 511 # being removed, restore that state.
504 512 entry = self._map[f]
505 513 if entry[0] == 'r' and entry[2] in (-1, -2):
506 514 source = self._copymap.get(f)
507 515 if entry[2] == -1:
508 516 self.merge(f)
509 517 elif entry[2] == -2:
510 518 self.otherparent(f)
511 519 if source:
512 520 self.copy(source, f)
513 521 return
514 522 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
515 523 return
516 524 self._addpath(f, 'n', 0, -1, -1)
517 525 if f in self._copymap:
518 526 del self._copymap[f]
519 527 if f in self._nonnormalset:
520 528 self._nonnormalset.remove(f)
521 529
522 530 def otherparent(self, f):
523 531 '''Mark as coming from the other parent, always dirty.'''
524 532 if self._pl[1] == nullid:
525 533 raise error.Abort(_("setting %r to other parent "
526 534 "only allowed in merges") % f)
527 535 if f in self and self[f] == 'n':
528 536 # merge-like
529 537 self._addpath(f, 'm', 0, -2, -1)
530 538 else:
531 539 # add-like
532 540 self._addpath(f, 'n', 0, -2, -1)
533 541
534 542 if f in self._copymap:
535 543 del self._copymap[f]
536 544
537 545 def add(self, f):
538 546 '''Mark a file added.'''
539 547 self._addpath(f, 'a', 0, -1, -1)
540 548 if f in self._copymap:
541 549 del self._copymap[f]
542 550
543 551 def remove(self, f):
544 552 '''Mark a file removed.'''
545 553 self._dirty = True
546 554 self._droppath(f)
547 555 size = 0
548 556 if self._pl[1] != nullid and f in self._map:
549 557 # backup the previous state
550 558 entry = self._map[f]
551 559 if entry[0] == 'm': # merge
552 560 size = -1
553 561 elif entry[0] == 'n' and entry[2] == -2: # other parent
554 562 size = -2
555 563 self._map[f] = dirstatetuple('r', 0, size, 0)
556 564 self._nonnormalset.add(f)
557 565 if size == 0 and f in self._copymap:
558 566 del self._copymap[f]
559 567
560 568 def merge(self, f):
561 569 '''Mark a file merged.'''
562 570 if self._pl[1] == nullid:
563 571 return self.normallookup(f)
564 572 return self.otherparent(f)
565 573
566 574 def drop(self, f):
567 575 '''Drop a file from the dirstate'''
568 576 if f in self._map:
569 577 self._dirty = True
570 578 self._droppath(f)
571 579 del self._map[f]
572 580 if f in self._nonnormalset:
573 581 self._nonnormalset.remove(f)
574 582 if f in self._copymap:
575 583 del self._copymap[f]
576 584
577 585 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
578 586 if exists is None:
579 587 exists = os.path.lexists(os.path.join(self._root, path))
580 588 if not exists:
581 589 # Maybe a path component exists
582 590 if not ignoremissing and '/' in path:
583 591 d, f = path.rsplit('/', 1)
584 592 d = self._normalize(d, False, ignoremissing, None)
585 593 folded = d + "/" + f
586 594 else:
587 595 # No path components, preserve original case
588 596 folded = path
589 597 else:
590 598 # recursively normalize leading directory components
591 599 # against dirstate
592 600 if '/' in normed:
593 601 d, f = normed.rsplit('/', 1)
594 602 d = self._normalize(d, False, ignoremissing, True)
595 603 r = self._root + "/" + d
596 604 folded = d + "/" + util.fspath(f, r)
597 605 else:
598 606 folded = util.fspath(normed, self._root)
599 607 storemap[normed] = folded
600 608
601 609 return folded
602 610
603 611 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
604 612 normed = util.normcase(path)
605 613 folded = self._filefoldmap.get(normed, None)
606 614 if folded is None:
607 615 if isknown:
608 616 folded = path
609 617 else:
610 618 folded = self._discoverpath(path, normed, ignoremissing, exists,
611 619 self._filefoldmap)
612 620 return folded
613 621
614 622 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
615 623 normed = util.normcase(path)
616 624 folded = self._filefoldmap.get(normed, None)
617 625 if folded is None:
618 626 folded = self._dirfoldmap.get(normed, None)
619 627 if folded is None:
620 628 if isknown:
621 629 folded = path
622 630 else:
623 631 # store discovered result in dirfoldmap so that future
624 632 # normalizefile calls don't start matching directories
625 633 folded = self._discoverpath(path, normed, ignoremissing, exists,
626 634 self._dirfoldmap)
627 635 return folded
628 636
629 637 def normalize(self, path, isknown=False, ignoremissing=False):
630 638 '''
631 639 normalize the case of a pathname when on a casefolding filesystem
632 640
633 641 isknown specifies whether the filename came from walking the
634 642 disk, to avoid extra filesystem access.
635 643
636 644 If ignoremissing is True, missing path are returned
637 645 unchanged. Otherwise, we try harder to normalize possibly
638 646 existing path components.
639 647
640 648 The normalized case is determined based on the following precedence:
641 649
642 650 - version of name already stored in the dirstate
643 651 - version of name stored on disk
644 652 - version provided via command arguments
645 653 '''
646 654
647 655 if self._checkcase:
648 656 return self._normalize(path, isknown, ignoremissing)
649 657 return path
650 658
651 659 def clear(self):
652 660 self._map = {}
653 661 self._nonnormalset = set()
654 662 if "_dirs" in self.__dict__:
655 663 delattr(self, "_dirs")
656 664 self._copymap = {}
657 665 self._pl = [nullid, nullid]
658 666 self._lastnormaltime = 0
667 self._updatedfiles.clear()
659 668 self._dirty = True
660 669
661 670 def rebuild(self, parent, allfiles, changedfiles=None):
662 671 if changedfiles is None:
663 672 # Rebuild entire dirstate
664 673 changedfiles = allfiles
665 674 lastnormaltime = self._lastnormaltime
666 675 self.clear()
667 676 self._lastnormaltime = lastnormaltime
668 677
669 678 if self._origpl is None:
670 679 self._origpl = self._pl
671 680 self._pl = (parent, nullid)
672 681 for f in changedfiles:
673 682 if f in allfiles:
674 683 self.normallookup(f)
675 684 else:
676 685 self.drop(f)
677 686
678 687 self._dirty = True
679 688
680 689 def write(self, tr):
681 690 if not self._dirty:
682 691 return
683 692
684 693 filename = self._filename
685 694 if tr:
686 695 # 'dirstate.write()' is not only for writing in-memory
687 696 # changes out, but also for dropping ambiguous timestamp.
688 697 # delayed writing re-raise "ambiguous timestamp issue".
689 698 # See also the wiki page below for detail:
690 699 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
691 700
692 701 # emulate dropping timestamp in 'parsers.pack_dirstate'
693 702 now = _getfsnow(self._opener)
694 703 dmap = self._map
695 for f, e in dmap.iteritems():
696 if e[0] == 'n' and e[3] == now:
704 for f in self._updatedfiles:
705 e = dmap.get(f)
706 if e is not None and e[0] == 'n' and e[3] == now:
697 707 dmap[f] = dirstatetuple(e[0], e[1], e[2], -1)
698 708 self._nonnormalset.add(f)
699 709
700 710 # emulate that all 'dirstate.normal' results are written out
701 711 self._lastnormaltime = 0
712 self._updatedfiles.clear()
702 713
703 714 # delay writing in-memory changes out
704 715 tr.addfilegenerator('dirstate', (self._filename,),
705 716 self._writedirstate, location='plain')
706 717 return
707 718
708 719 st = self._opener(filename, "w", atomictemp=True, checkambig=True)
709 720 self._writedirstate(st)
710 721
711 722 def addparentchangecallback(self, category, callback):
712 723 """add a callback to be called when the wd parents are changed
713 724
714 725 Callback will be called with the following arguments:
715 726 dirstate, (oldp1, oldp2), (newp1, newp2)
716 727
717 728 Category is a unique identifier to allow overwriting an old callback
718 729 with a newer callback.
719 730 """
720 731 self._plchangecallbacks[category] = callback
721 732
722 733 def _writedirstate(self, st):
723 734 # notify callbacks about parents change
724 735 if self._origpl is not None and self._origpl != self._pl:
725 736 for c, callback in sorted(self._plchangecallbacks.iteritems()):
726 737 callback(self, self._origpl, self._pl)
727 738 self._origpl = None
728 739 # use the modification time of the newly created temporary file as the
729 740 # filesystem's notion of 'now'
730 741 now = util.fstat(st).st_mtime & _rangemask
731 742
732 743 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
733 744 # timestamp of each entries in dirstate, because of 'now > mtime'
734 745 delaywrite = self._ui.configint('debug', 'dirstate.delaywrite', 0)
735 746 if delaywrite > 0:
736 747 # do we have any files to delay for?
737 748 for f, e in self._map.iteritems():
738 749 if e[0] == 'n' and e[3] == now:
739 750 import time # to avoid useless import
740 751 # rather than sleep n seconds, sleep until the next
741 752 # multiple of n seconds
742 753 clock = time.time()
743 754 start = int(clock) - (int(clock) % delaywrite)
744 755 end = start + delaywrite
745 756 time.sleep(end - clock)
746 757 now = end # trust our estimate that the end is near now
747 758 break
748 759
749 760 st.write(parsers.pack_dirstate(self._map, self._copymap, self._pl, now))
750 761 self._nonnormalset = nonnormalentries(self._map)
751 762 st.close()
752 763 self._lastnormaltime = 0
753 764 self._dirty = self._dirtypl = False
754 765
755 766 def _dirignore(self, f):
756 767 if f == '.':
757 768 return False
758 769 if self._ignore(f):
759 770 return True
760 771 for p in util.finddirs(f):
761 772 if self._ignore(p):
762 773 return True
763 774 return False
764 775
765 776 def _ignorefiles(self):
766 777 files = []
767 778 if os.path.exists(self._join('.hgignore')):
768 779 files.append(self._join('.hgignore'))
769 780 for name, path in self._ui.configitems("ui"):
770 781 if name == 'ignore' or name.startswith('ignore.'):
771 782 # we need to use os.path.join here rather than self._join
772 783 # because path is arbitrary and user-specified
773 784 files.append(os.path.join(self._rootdir, util.expandpath(path)))
774 785 return files
775 786
776 787 def _ignorefileandline(self, f):
777 788 files = collections.deque(self._ignorefiles())
778 789 visited = set()
779 790 while files:
780 791 i = files.popleft()
781 792 patterns = matchmod.readpatternfile(i, self._ui.warn,
782 793 sourceinfo=True)
783 794 for pattern, lineno, line in patterns:
784 795 kind, p = matchmod._patsplit(pattern, 'glob')
785 796 if kind == "subinclude":
786 797 if p not in visited:
787 798 files.append(p)
788 799 continue
789 800 m = matchmod.match(self._root, '', [], [pattern],
790 801 warn=self._ui.warn)
791 802 if m(f):
792 803 return (i, lineno, line)
793 804 visited.add(i)
794 805 return (None, -1, "")
795 806
796 807 def _walkexplicit(self, match, subrepos):
797 808 '''Get stat data about the files explicitly specified by match.
798 809
799 810 Return a triple (results, dirsfound, dirsnotfound).
800 811 - results is a mapping from filename to stat result. It also contains
801 812 listings mapping subrepos and .hg to None.
802 813 - dirsfound is a list of files found to be directories.
803 814 - dirsnotfound is a list of files that the dirstate thinks are
804 815 directories and that were not found.'''
805 816
806 817 def badtype(mode):
807 818 kind = _('unknown')
808 819 if stat.S_ISCHR(mode):
809 820 kind = _('character device')
810 821 elif stat.S_ISBLK(mode):
811 822 kind = _('block device')
812 823 elif stat.S_ISFIFO(mode):
813 824 kind = _('fifo')
814 825 elif stat.S_ISSOCK(mode):
815 826 kind = _('socket')
816 827 elif stat.S_ISDIR(mode):
817 828 kind = _('directory')
818 829 return _('unsupported file type (type is %s)') % kind
819 830
820 831 matchedir = match.explicitdir
821 832 badfn = match.bad
822 833 dmap = self._map
823 834 lstat = os.lstat
824 835 getkind = stat.S_IFMT
825 836 dirkind = stat.S_IFDIR
826 837 regkind = stat.S_IFREG
827 838 lnkkind = stat.S_IFLNK
828 839 join = self._join
829 840 dirsfound = []
830 841 foundadd = dirsfound.append
831 842 dirsnotfound = []
832 843 notfoundadd = dirsnotfound.append
833 844
834 845 if not match.isexact() and self._checkcase:
835 846 normalize = self._normalize
836 847 else:
837 848 normalize = None
838 849
839 850 files = sorted(match.files())
840 851 subrepos.sort()
841 852 i, j = 0, 0
842 853 while i < len(files) and j < len(subrepos):
843 854 subpath = subrepos[j] + "/"
844 855 if files[i] < subpath:
845 856 i += 1
846 857 continue
847 858 while i < len(files) and files[i].startswith(subpath):
848 859 del files[i]
849 860 j += 1
850 861
851 862 if not files or '.' in files:
852 863 files = ['.']
853 864 results = dict.fromkeys(subrepos)
854 865 results['.hg'] = None
855 866
856 867 alldirs = None
857 868 for ff in files:
858 869 # constructing the foldmap is expensive, so don't do it for the
859 870 # common case where files is ['.']
860 871 if normalize and ff != '.':
861 872 nf = normalize(ff, False, True)
862 873 else:
863 874 nf = ff
864 875 if nf in results:
865 876 continue
866 877
867 878 try:
868 879 st = lstat(join(nf))
869 880 kind = getkind(st.st_mode)
870 881 if kind == dirkind:
871 882 if nf in dmap:
872 883 # file replaced by dir on disk but still in dirstate
873 884 results[nf] = None
874 885 if matchedir:
875 886 matchedir(nf)
876 887 foundadd((nf, ff))
877 888 elif kind == regkind or kind == lnkkind:
878 889 results[nf] = st
879 890 else:
880 891 badfn(ff, badtype(kind))
881 892 if nf in dmap:
882 893 results[nf] = None
883 894 except OSError as inst: # nf not found on disk - it is dirstate only
884 895 if nf in dmap: # does it exactly match a missing file?
885 896 results[nf] = None
886 897 else: # does it match a missing directory?
887 898 if alldirs is None:
888 899 alldirs = util.dirs(dmap)
889 900 if nf in alldirs:
890 901 if matchedir:
891 902 matchedir(nf)
892 903 notfoundadd(nf)
893 904 else:
894 905 badfn(ff, inst.strerror)
895 906
896 907 # Case insensitive filesystems cannot rely on lstat() failing to detect
897 908 # a case-only rename. Prune the stat object for any file that does not
898 909 # match the case in the filesystem, if there are multiple files that
899 910 # normalize to the same path.
900 911 if match.isexact() and self._checkcase:
901 912 normed = {}
902 913
903 914 for f, st in results.iteritems():
904 915 if st is None:
905 916 continue
906 917
907 918 nc = util.normcase(f)
908 919 paths = normed.get(nc)
909 920
910 921 if paths is None:
911 922 paths = set()
912 923 normed[nc] = paths
913 924
914 925 paths.add(f)
915 926
916 927 for norm, paths in normed.iteritems():
917 928 if len(paths) > 1:
918 929 for path in paths:
919 930 folded = self._discoverpath(path, norm, True, None,
920 931 self._dirfoldmap)
921 932 if path != folded:
922 933 results[path] = None
923 934
924 935 return results, dirsfound, dirsnotfound
925 936
926 937 def walk(self, match, subrepos, unknown, ignored, full=True):
927 938 '''
928 939 Walk recursively through the directory tree, finding all files
929 940 matched by match.
930 941
931 942 If full is False, maybe skip some known-clean files.
932 943
933 944 Return a dict mapping filename to stat-like object (either
934 945 mercurial.osutil.stat instance or return value of os.stat()).
935 946
936 947 '''
937 948 # full is a flag that extensions that hook into walk can use -- this
938 949 # implementation doesn't use it at all. This satisfies the contract
939 950 # because we only guarantee a "maybe".
940 951
941 952 if ignored:
942 953 ignore = util.never
943 954 dirignore = util.never
944 955 elif unknown:
945 956 ignore = self._ignore
946 957 dirignore = self._dirignore
947 958 else:
948 959 # if not unknown and not ignored, drop dir recursion and step 2
949 960 ignore = util.always
950 961 dirignore = util.always
951 962
952 963 matchfn = match.matchfn
953 964 matchalways = match.always()
954 965 matchtdir = match.traversedir
955 966 dmap = self._map
956 967 listdir = osutil.listdir
957 968 lstat = os.lstat
958 969 dirkind = stat.S_IFDIR
959 970 regkind = stat.S_IFREG
960 971 lnkkind = stat.S_IFLNK
961 972 join = self._join
962 973
963 974 exact = skipstep3 = False
964 975 if match.isexact(): # match.exact
965 976 exact = True
966 977 dirignore = util.always # skip step 2
967 978 elif match.prefix(): # match.match, no patterns
968 979 skipstep3 = True
969 980
970 981 if not exact and self._checkcase:
971 982 normalize = self._normalize
972 983 normalizefile = self._normalizefile
973 984 skipstep3 = False
974 985 else:
975 986 normalize = self._normalize
976 987 normalizefile = None
977 988
978 989 # step 1: find all explicit files
979 990 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
980 991
981 992 skipstep3 = skipstep3 and not (work or dirsnotfound)
982 993 work = [d for d in work if not dirignore(d[0])]
983 994
984 995 # step 2: visit subdirectories
985 996 def traverse(work, alreadynormed):
986 997 wadd = work.append
987 998 while work:
988 999 nd = work.pop()
989 1000 skip = None
990 1001 if nd == '.':
991 1002 nd = ''
992 1003 else:
993 1004 skip = '.hg'
994 1005 try:
995 1006 entries = listdir(join(nd), stat=True, skip=skip)
996 1007 except OSError as inst:
997 1008 if inst.errno in (errno.EACCES, errno.ENOENT):
998 1009 match.bad(self.pathto(nd), inst.strerror)
999 1010 continue
1000 1011 raise
1001 1012 for f, kind, st in entries:
1002 1013 if normalizefile:
1003 1014 # even though f might be a directory, we're only
1004 1015 # interested in comparing it to files currently in the
1005 1016 # dmap -- therefore normalizefile is enough
1006 1017 nf = normalizefile(nd and (nd + "/" + f) or f, True,
1007 1018 True)
1008 1019 else:
1009 1020 nf = nd and (nd + "/" + f) or f
1010 1021 if nf not in results:
1011 1022 if kind == dirkind:
1012 1023 if not ignore(nf):
1013 1024 if matchtdir:
1014 1025 matchtdir(nf)
1015 1026 wadd(nf)
1016 1027 if nf in dmap and (matchalways or matchfn(nf)):
1017 1028 results[nf] = None
1018 1029 elif kind == regkind or kind == lnkkind:
1019 1030 if nf in dmap:
1020 1031 if matchalways or matchfn(nf):
1021 1032 results[nf] = st
1022 1033 elif ((matchalways or matchfn(nf))
1023 1034 and not ignore(nf)):
1024 1035 # unknown file -- normalize if necessary
1025 1036 if not alreadynormed:
1026 1037 nf = normalize(nf, False, True)
1027 1038 results[nf] = st
1028 1039 elif nf in dmap and (matchalways or matchfn(nf)):
1029 1040 results[nf] = None
1030 1041
1031 1042 for nd, d in work:
1032 1043 # alreadynormed means that processwork doesn't have to do any
1033 1044 # expensive directory normalization
1034 1045 alreadynormed = not normalize or nd == d
1035 1046 traverse([d], alreadynormed)
1036 1047
1037 1048 for s in subrepos:
1038 1049 del results[s]
1039 1050 del results['.hg']
1040 1051
1041 1052 # step 3: visit remaining files from dmap
1042 1053 if not skipstep3 and not exact:
1043 1054 # If a dmap file is not in results yet, it was either
1044 1055 # a) not matching matchfn b) ignored, c) missing, or d) under a
1045 1056 # symlink directory.
1046 1057 if not results and matchalways:
1047 1058 visit = dmap.keys()
1048 1059 else:
1049 1060 visit = [f for f in dmap if f not in results and matchfn(f)]
1050 1061 visit.sort()
1051 1062
1052 1063 if unknown:
1053 1064 # unknown == True means we walked all dirs under the roots
1054 1065 # that wasn't ignored, and everything that matched was stat'ed
1055 1066 # and is already in results.
1056 1067 # The rest must thus be ignored or under a symlink.
1057 1068 audit_path = pathutil.pathauditor(self._root)
1058 1069
1059 1070 for nf in iter(visit):
1060 1071 # If a stat for the same file was already added with a
1061 1072 # different case, don't add one for this, since that would
1062 1073 # make it appear as if the file exists under both names
1063 1074 # on disk.
1064 1075 if (normalizefile and
1065 1076 normalizefile(nf, True, True) in results):
1066 1077 results[nf] = None
1067 1078 # Report ignored items in the dmap as long as they are not
1068 1079 # under a symlink directory.
1069 1080 elif audit_path.check(nf):
1070 1081 try:
1071 1082 results[nf] = lstat(join(nf))
1072 1083 # file was just ignored, no links, and exists
1073 1084 except OSError:
1074 1085 # file doesn't exist
1075 1086 results[nf] = None
1076 1087 else:
1077 1088 # It's either missing or under a symlink directory
1078 1089 # which we in this case report as missing
1079 1090 results[nf] = None
1080 1091 else:
1081 1092 # We may not have walked the full directory tree above,
1082 1093 # so stat and check everything we missed.
1083 1094 nf = iter(visit).next
1084 1095 for st in util.statfiles([join(i) for i in visit]):
1085 1096 results[nf()] = st
1086 1097 return results
1087 1098
1088 1099 def status(self, match, subrepos, ignored, clean, unknown):
1089 1100 '''Determine the status of the working copy relative to the
1090 1101 dirstate and return a pair of (unsure, status), where status is of type
1091 1102 scmutil.status and:
1092 1103
1093 1104 unsure:
1094 1105 files that might have been modified since the dirstate was
1095 1106 written, but need to be read to be sure (size is the same
1096 1107 but mtime differs)
1097 1108 status.modified:
1098 1109 files that have definitely been modified since the dirstate
1099 1110 was written (different size or mode)
1100 1111 status.clean:
1101 1112 files that have definitely not been modified since the
1102 1113 dirstate was written
1103 1114 '''
1104 1115 listignored, listclean, listunknown = ignored, clean, unknown
1105 1116 lookup, modified, added, unknown, ignored = [], [], [], [], []
1106 1117 removed, deleted, clean = [], [], []
1107 1118
1108 1119 dmap = self._map
1109 1120 ladd = lookup.append # aka "unsure"
1110 1121 madd = modified.append
1111 1122 aadd = added.append
1112 1123 uadd = unknown.append
1113 1124 iadd = ignored.append
1114 1125 radd = removed.append
1115 1126 dadd = deleted.append
1116 1127 cadd = clean.append
1117 1128 mexact = match.exact
1118 1129 dirignore = self._dirignore
1119 1130 checkexec = self._checkexec
1120 1131 copymap = self._copymap
1121 1132 lastnormaltime = self._lastnormaltime
1122 1133
1123 1134 # We need to do full walks when either
1124 1135 # - we're listing all clean files, or
1125 1136 # - match.traversedir does something, because match.traversedir should
1126 1137 # be called for every dir in the working dir
1127 1138 full = listclean or match.traversedir is not None
1128 1139 for fn, st in self.walk(match, subrepos, listunknown, listignored,
1129 1140 full=full).iteritems():
1130 1141 if fn not in dmap:
1131 1142 if (listignored or mexact(fn)) and dirignore(fn):
1132 1143 if listignored:
1133 1144 iadd(fn)
1134 1145 else:
1135 1146 uadd(fn)
1136 1147 continue
1137 1148
1138 1149 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1139 1150 # written like that for performance reasons. dmap[fn] is not a
1140 1151 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1141 1152 # opcode has fast paths when the value to be unpacked is a tuple or
1142 1153 # a list, but falls back to creating a full-fledged iterator in
1143 1154 # general. That is much slower than simply accessing and storing the
1144 1155 # tuple members one by one.
1145 1156 t = dmap[fn]
1146 1157 state = t[0]
1147 1158 mode = t[1]
1148 1159 size = t[2]
1149 1160 time = t[3]
1150 1161
1151 1162 if not st and state in "nma":
1152 1163 dadd(fn)
1153 1164 elif state == 'n':
1154 1165 if (size >= 0 and
1155 1166 ((size != st.st_size and size != st.st_size & _rangemask)
1156 1167 or ((mode ^ st.st_mode) & 0o100 and checkexec))
1157 1168 or size == -2 # other parent
1158 1169 or fn in copymap):
1159 1170 madd(fn)
1160 1171 elif time != st.st_mtime and time != st.st_mtime & _rangemask:
1161 1172 ladd(fn)
1162 1173 elif st.st_mtime == lastnormaltime:
1163 1174 # fn may have just been marked as normal and it may have
1164 1175 # changed in the same second without changing its size.
1165 1176 # This can happen if we quickly do multiple commits.
1166 1177 # Force lookup, so we don't miss such a racy file change.
1167 1178 ladd(fn)
1168 1179 elif listclean:
1169 1180 cadd(fn)
1170 1181 elif state == 'm':
1171 1182 madd(fn)
1172 1183 elif state == 'a':
1173 1184 aadd(fn)
1174 1185 elif state == 'r':
1175 1186 radd(fn)
1176 1187
1177 1188 return (lookup, scmutil.status(modified, added, removed, deleted,
1178 1189 unknown, ignored, clean))
1179 1190
1180 1191 def matches(self, match):
1181 1192 '''
1182 1193 return files in the dirstate (in whatever state) filtered by match
1183 1194 '''
1184 1195 dmap = self._map
1185 1196 if match.always():
1186 1197 return dmap.keys()
1187 1198 files = match.files()
1188 1199 if match.isexact():
1189 1200 # fast path -- filter the other way around, since typically files is
1190 1201 # much smaller than dmap
1191 1202 return [f for f in files if f in dmap]
1192 1203 if match.prefix() and all(fn in dmap for fn in files):
1193 1204 # fast path -- all the values are known to be files, so just return
1194 1205 # that
1195 1206 return list(files)
1196 1207 return [f for f in dmap if match(f)]
1197 1208
1198 1209 def _actualfilename(self, tr):
1199 1210 if tr:
1200 1211 return self._pendingfilename
1201 1212 else:
1202 1213 return self._filename
1203 1214
1204 1215 def savebackup(self, tr, suffix='', prefix=''):
1205 1216 '''Save current dirstate into backup file with suffix'''
1206 1217 assert len(suffix) > 0 or len(prefix) > 0
1207 1218 filename = self._actualfilename(tr)
1208 1219
1209 1220 # use '_writedirstate' instead of 'write' to write changes certainly,
1210 1221 # because the latter omits writing out if transaction is running.
1211 1222 # output file will be used to create backup of dirstate at this point.
1212 1223 self._writedirstate(self._opener(filename, "w", atomictemp=True,
1213 1224 checkambig=True))
1214 1225
1215 1226 if tr:
1216 1227 # ensure that subsequent tr.writepending returns True for
1217 1228 # changes written out above, even if dirstate is never
1218 1229 # changed after this
1219 1230 tr.addfilegenerator('dirstate', (self._filename,),
1220 1231 self._writedirstate, location='plain')
1221 1232
1222 1233 # ensure that pending file written above is unlinked at
1223 1234 # failure, even if tr.writepending isn't invoked until the
1224 1235 # end of this transaction
1225 1236 tr.registertmp(filename, location='plain')
1226 1237
1227 1238 self._opener.write(prefix + self._filename + suffix,
1228 1239 self._opener.tryread(filename))
1229 1240
1230 1241 def restorebackup(self, tr, suffix='', prefix=''):
1231 1242 '''Restore dirstate by backup file with suffix'''
1232 1243 assert len(suffix) > 0 or len(prefix) > 0
1233 1244 # this "invalidate()" prevents "wlock.release()" from writing
1234 1245 # changes of dirstate out after restoring from backup file
1235 1246 self.invalidate()
1236 1247 filename = self._actualfilename(tr)
1237 1248 # using self._filename to avoid having "pending" in the backup filename
1238 1249 self._opener.rename(prefix + self._filename + suffix, filename,
1239 1250 checkambig=True)
1240 1251
1241 1252 def clearbackup(self, tr, suffix='', prefix=''):
1242 1253 '''Clear backup file with suffix'''
1243 1254 assert len(suffix) > 0 or len(prefix) > 0
1244 1255 # using self._filename to avoid having "pending" in the backup filename
1245 1256 self._opener.unlink(prefix + self._filename + suffix)
General Comments 0
You need to be logged in to leave comments. Login now