##// END OF EJS Templates
dirstate: isolate node len dependency for the pure version...
Joerg Sonnenberger -
r45778:4f0e03d9 default
parent child Browse files
Show More
@@ -1,1927 +1,1933
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 contextlib
12 12 import errno
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from .node import nullid
18 18 from .pycompat import delattr
19 19
20 20 from hgdemandimport import tracing
21 21
22 22 from . import (
23 23 encoding,
24 24 error,
25 25 match as matchmod,
26 26 pathutil,
27 27 policy,
28 28 pycompat,
29 29 scmutil,
30 30 sparse,
31 31 txnutil,
32 32 util,
33 33 )
34 34
35 35 from .interfaces import (
36 36 dirstate as intdirstate,
37 37 util as interfaceutil,
38 38 )
39 39
40 40 parsers = policy.importmod('parsers')
41 41 rustmod = policy.importrust('dirstate')
42 42
43 43 propertycache = util.propertycache
44 44 filecache = scmutil.filecache
45 45 _rangemask = 0x7FFFFFFF
46 46
47 47 dirstatetuple = parsers.dirstatetuple
48 48
49 49
50 50 class repocache(filecache):
51 51 """filecache for files in .hg/"""
52 52
53 53 def join(self, obj, fname):
54 54 return obj._opener.join(fname)
55 55
56 56
57 57 class rootcache(filecache):
58 58 """filecache for files in the repository root"""
59 59
60 60 def join(self, obj, fname):
61 61 return obj._join(fname)
62 62
63 63
64 64 def _getfsnow(vfs):
65 65 '''Get "now" timestamp on filesystem'''
66 66 tmpfd, tmpname = vfs.mkstemp()
67 67 try:
68 68 return os.fstat(tmpfd)[stat.ST_MTIME]
69 69 finally:
70 70 os.close(tmpfd)
71 71 vfs.unlink(tmpname)
72 72
73 73
74 74 @interfaceutil.implementer(intdirstate.idirstate)
75 75 class dirstate(object):
76 76 def __init__(self, opener, ui, root, validate, sparsematchfn):
77 77 '''Create a new dirstate object.
78 78
79 79 opener is an open()-like callable that can be used to open the
80 80 dirstate file; root is the root of the directory tracked by
81 81 the dirstate.
82 82 '''
83 83 self._opener = opener
84 84 self._validate = validate
85 85 self._root = root
86 86 self._sparsematchfn = sparsematchfn
87 87 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
88 88 # UNC path pointing to root share (issue4557)
89 89 self._rootdir = pathutil.normasprefix(root)
90 90 self._dirty = False
91 91 self._lastnormaltime = 0
92 92 self._ui = ui
93 93 self._filecache = {}
94 94 self._parentwriters = 0
95 95 self._filename = b'dirstate'
96 96 self._pendingfilename = b'%s.pending' % self._filename
97 97 self._plchangecallbacks = {}
98 98 self._origpl = None
99 99 self._updatedfiles = set()
100 100 self._mapcls = dirstatemap
101 101 # Access and cache cwd early, so we don't access it for the first time
102 102 # after a working-copy update caused it to not exist (accessing it then
103 103 # raises an exception).
104 104 self._cwd
105 105
106 106 def prefetch_parents(self):
107 107 """make sure the parents are loaded
108 108
109 109 Used to avoid a race condition.
110 110 """
111 111 self._pl
112 112
113 113 @contextlib.contextmanager
114 114 def parentchange(self):
115 115 '''Context manager for handling dirstate parents.
116 116
117 117 If an exception occurs in the scope of the context manager,
118 118 the incoherent dirstate won't be written when wlock is
119 119 released.
120 120 '''
121 121 self._parentwriters += 1
122 122 yield
123 123 # Typically we want the "undo" step of a context manager in a
124 124 # finally block so it happens even when an exception
125 125 # occurs. In this case, however, we only want to decrement
126 126 # parentwriters if the code in the with statement exits
127 127 # normally, so we don't have a try/finally here on purpose.
128 128 self._parentwriters -= 1
129 129
130 130 def pendingparentchange(self):
131 131 '''Returns true if the dirstate is in the middle of a set of changes
132 132 that modify the dirstate parent.
133 133 '''
134 134 return self._parentwriters > 0
135 135
136 136 @propertycache
137 137 def _map(self):
138 138 """Return the dirstate contents (see documentation for dirstatemap)."""
139 139 self._map = self._mapcls(self._ui, self._opener, self._root)
140 140 return self._map
141 141
142 142 @property
143 143 def _sparsematcher(self):
144 144 """The matcher for the sparse checkout.
145 145
146 146 The working directory may not include every file from a manifest. The
147 147 matcher obtained by this property will match a path if it is to be
148 148 included in the working directory.
149 149 """
150 150 # TODO there is potential to cache this property. For now, the matcher
151 151 # is resolved on every access. (But the called function does use a
152 152 # cache to keep the lookup fast.)
153 153 return self._sparsematchfn()
154 154
155 155 @repocache(b'branch')
156 156 def _branch(self):
157 157 try:
158 158 return self._opener.read(b"branch").strip() or b"default"
159 159 except IOError as inst:
160 160 if inst.errno != errno.ENOENT:
161 161 raise
162 162 return b"default"
163 163
164 164 @property
165 165 def _pl(self):
166 166 return self._map.parents()
167 167
168 168 def hasdir(self, d):
169 169 return self._map.hastrackeddir(d)
170 170
171 171 @rootcache(b'.hgignore')
172 172 def _ignore(self):
173 173 files = self._ignorefiles()
174 174 if not files:
175 175 return matchmod.never()
176 176
177 177 pats = [b'include:%s' % f for f in files]
178 178 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
179 179
180 180 @propertycache
181 181 def _slash(self):
182 182 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
183 183
184 184 @propertycache
185 185 def _checklink(self):
186 186 return util.checklink(self._root)
187 187
188 188 @propertycache
189 189 def _checkexec(self):
190 190 return bool(util.checkexec(self._root))
191 191
192 192 @propertycache
193 193 def _checkcase(self):
194 194 return not util.fscasesensitive(self._join(b'.hg'))
195 195
196 196 def _join(self, f):
197 197 # much faster than os.path.join()
198 198 # it's safe because f is always a relative path
199 199 return self._rootdir + f
200 200
201 201 def flagfunc(self, buildfallback):
202 202 if self._checklink and self._checkexec:
203 203
204 204 def f(x):
205 205 try:
206 206 st = os.lstat(self._join(x))
207 207 if util.statislink(st):
208 208 return b'l'
209 209 if util.statisexec(st):
210 210 return b'x'
211 211 except OSError:
212 212 pass
213 213 return b''
214 214
215 215 return f
216 216
217 217 fallback = buildfallback()
218 218 if self._checklink:
219 219
220 220 def f(x):
221 221 if os.path.islink(self._join(x)):
222 222 return b'l'
223 223 if b'x' in fallback(x):
224 224 return b'x'
225 225 return b''
226 226
227 227 return f
228 228 if self._checkexec:
229 229
230 230 def f(x):
231 231 if b'l' in fallback(x):
232 232 return b'l'
233 233 if util.isexec(self._join(x)):
234 234 return b'x'
235 235 return b''
236 236
237 237 return f
238 238 else:
239 239 return fallback
240 240
241 241 @propertycache
242 242 def _cwd(self):
243 243 # internal config: ui.forcecwd
244 244 forcecwd = self._ui.config(b'ui', b'forcecwd')
245 245 if forcecwd:
246 246 return forcecwd
247 247 return encoding.getcwd()
248 248
249 249 def getcwd(self):
250 250 '''Return the path from which a canonical path is calculated.
251 251
252 252 This path should be used to resolve file patterns or to convert
253 253 canonical paths back to file paths for display. It shouldn't be
254 254 used to get real file paths. Use vfs functions instead.
255 255 '''
256 256 cwd = self._cwd
257 257 if cwd == self._root:
258 258 return b''
259 259 # self._root ends with a path separator if self._root is '/' or 'C:\'
260 260 rootsep = self._root
261 261 if not util.endswithsep(rootsep):
262 262 rootsep += pycompat.ossep
263 263 if cwd.startswith(rootsep):
264 264 return cwd[len(rootsep) :]
265 265 else:
266 266 # we're outside the repo. return an absolute path.
267 267 return cwd
268 268
269 269 def pathto(self, f, cwd=None):
270 270 if cwd is None:
271 271 cwd = self.getcwd()
272 272 path = util.pathto(self._root, cwd, f)
273 273 if self._slash:
274 274 return util.pconvert(path)
275 275 return path
276 276
277 277 def __getitem__(self, key):
278 278 '''Return the current state of key (a filename) in the dirstate.
279 279
280 280 States are:
281 281 n normal
282 282 m needs merging
283 283 r marked for removal
284 284 a marked for addition
285 285 ? not tracked
286 286 '''
287 287 return self._map.get(key, (b"?",))[0]
288 288
289 289 def __contains__(self, key):
290 290 return key in self._map
291 291
292 292 def __iter__(self):
293 293 return iter(sorted(self._map))
294 294
295 295 def items(self):
296 296 return pycompat.iteritems(self._map)
297 297
298 298 iteritems = items
299 299
300 300 def parents(self):
301 301 return [self._validate(p) for p in self._pl]
302 302
303 303 def p1(self):
304 304 return self._validate(self._pl[0])
305 305
306 306 def p2(self):
307 307 return self._validate(self._pl[1])
308 308
309 309 def branch(self):
310 310 return encoding.tolocal(self._branch)
311 311
312 312 def setparents(self, p1, p2=nullid):
313 313 """Set dirstate parents to p1 and p2.
314 314
315 315 When moving from two parents to one, 'm' merged entries a
316 316 adjusted to normal and previous copy records discarded and
317 317 returned by the call.
318 318
319 319 See localrepo.setparents()
320 320 """
321 321 if self._parentwriters == 0:
322 322 raise ValueError(
323 323 b"cannot set dirstate parent outside of "
324 324 b"dirstate.parentchange context manager"
325 325 )
326 326
327 327 self._dirty = True
328 328 oldp2 = self._pl[1]
329 329 if self._origpl is None:
330 330 self._origpl = self._pl
331 331 self._map.setparents(p1, p2)
332 332 copies = {}
333 333 if oldp2 != nullid and p2 == nullid:
334 334 candidatefiles = self._map.nonnormalset.union(
335 335 self._map.otherparentset
336 336 )
337 337 for f in candidatefiles:
338 338 s = self._map.get(f)
339 339 if s is None:
340 340 continue
341 341
342 342 # Discard 'm' markers when moving away from a merge state
343 343 if s[0] == b'm':
344 344 source = self._map.copymap.get(f)
345 345 if source:
346 346 copies[f] = source
347 347 self.normallookup(f)
348 348 # Also fix up otherparent markers
349 349 elif s[0] == b'n' and s[2] == -2:
350 350 source = self._map.copymap.get(f)
351 351 if source:
352 352 copies[f] = source
353 353 self.add(f)
354 354 return copies
355 355
356 356 def setbranch(self, branch):
357 357 self.__class__._branch.set(self, encoding.fromlocal(branch))
358 358 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
359 359 try:
360 360 f.write(self._branch + b'\n')
361 361 f.close()
362 362
363 363 # make sure filecache has the correct stat info for _branch after
364 364 # replacing the underlying file
365 365 ce = self._filecache[b'_branch']
366 366 if ce:
367 367 ce.refresh()
368 368 except: # re-raises
369 369 f.discard()
370 370 raise
371 371
372 372 def invalidate(self):
373 373 '''Causes the next access to reread the dirstate.
374 374
375 375 This is different from localrepo.invalidatedirstate() because it always
376 376 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
377 377 check whether the dirstate has changed before rereading it.'''
378 378
379 379 for a in ("_map", "_branch", "_ignore"):
380 380 if a in self.__dict__:
381 381 delattr(self, a)
382 382 self._lastnormaltime = 0
383 383 self._dirty = False
384 384 self._updatedfiles.clear()
385 385 self._parentwriters = 0
386 386 self._origpl = None
387 387
388 388 def copy(self, source, dest):
389 389 """Mark dest as a copy of source. Unmark dest if source is None."""
390 390 if source == dest:
391 391 return
392 392 self._dirty = True
393 393 if source is not None:
394 394 self._map.copymap[dest] = source
395 395 self._updatedfiles.add(source)
396 396 self._updatedfiles.add(dest)
397 397 elif self._map.copymap.pop(dest, None):
398 398 self._updatedfiles.add(dest)
399 399
400 400 def copied(self, file):
401 401 return self._map.copymap.get(file, None)
402 402
403 403 def copies(self):
404 404 return self._map.copymap
405 405
406 406 def _addpath(self, f, state, mode, size, mtime):
407 407 oldstate = self[f]
408 408 if state == b'a' or oldstate == b'r':
409 409 scmutil.checkfilename(f)
410 410 if self._map.hastrackeddir(f):
411 411 raise error.Abort(
412 412 _(b'directory %r already in dirstate') % pycompat.bytestr(f)
413 413 )
414 414 # shadows
415 415 for d in pathutil.finddirs(f):
416 416 if self._map.hastrackeddir(d):
417 417 break
418 418 entry = self._map.get(d)
419 419 if entry is not None and entry[0] != b'r':
420 420 raise error.Abort(
421 421 _(b'file %r in dirstate clashes with %r')
422 422 % (pycompat.bytestr(d), pycompat.bytestr(f))
423 423 )
424 424 self._dirty = True
425 425 self._updatedfiles.add(f)
426 426 self._map.addfile(f, oldstate, state, mode, size, mtime)
427 427
428 428 def normal(self, f, parentfiledata=None):
429 429 '''Mark a file normal and clean.
430 430
431 431 parentfiledata: (mode, size, mtime) of the clean file
432 432
433 433 parentfiledata should be computed from memory (for mode,
434 434 size), as or close as possible from the point where we
435 435 determined the file was clean, to limit the risk of the
436 436 file having been changed by an external process between the
437 437 moment where the file was determined to be clean and now.'''
438 438 if parentfiledata:
439 439 (mode, size, mtime) = parentfiledata
440 440 else:
441 441 s = os.lstat(self._join(f))
442 442 mode = s.st_mode
443 443 size = s.st_size
444 444 mtime = s[stat.ST_MTIME]
445 445 self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask)
446 446 self._map.copymap.pop(f, None)
447 447 if f in self._map.nonnormalset:
448 448 self._map.nonnormalset.remove(f)
449 449 if mtime > self._lastnormaltime:
450 450 # Remember the most recent modification timeslot for status(),
451 451 # to make sure we won't miss future size-preserving file content
452 452 # modifications that happen within the same timeslot.
453 453 self._lastnormaltime = mtime
454 454
455 455 def normallookup(self, f):
456 456 '''Mark a file normal, but possibly dirty.'''
457 457 if self._pl[1] != nullid:
458 458 # if there is a merge going on and the file was either
459 459 # in state 'm' (-1) or coming from other parent (-2) before
460 460 # being removed, restore that state.
461 461 entry = self._map.get(f)
462 462 if entry is not None:
463 463 if entry[0] == b'r' and entry[2] in (-1, -2):
464 464 source = self._map.copymap.get(f)
465 465 if entry[2] == -1:
466 466 self.merge(f)
467 467 elif entry[2] == -2:
468 468 self.otherparent(f)
469 469 if source:
470 470 self.copy(source, f)
471 471 return
472 472 if entry[0] == b'm' or entry[0] == b'n' and entry[2] == -2:
473 473 return
474 474 self._addpath(f, b'n', 0, -1, -1)
475 475 self._map.copymap.pop(f, None)
476 476
477 477 def otherparent(self, f):
478 478 '''Mark as coming from the other parent, always dirty.'''
479 479 if self._pl[1] == nullid:
480 480 raise error.Abort(
481 481 _(b"setting %r to other parent only allowed in merges") % f
482 482 )
483 483 if f in self and self[f] == b'n':
484 484 # merge-like
485 485 self._addpath(f, b'm', 0, -2, -1)
486 486 else:
487 487 # add-like
488 488 self._addpath(f, b'n', 0, -2, -1)
489 489 self._map.copymap.pop(f, None)
490 490
491 491 def add(self, f):
492 492 '''Mark a file added.'''
493 493 self._addpath(f, b'a', 0, -1, -1)
494 494 self._map.copymap.pop(f, None)
495 495
496 496 def remove(self, f):
497 497 '''Mark a file removed.'''
498 498 self._dirty = True
499 499 oldstate = self[f]
500 500 size = 0
501 501 if self._pl[1] != nullid:
502 502 entry = self._map.get(f)
503 503 if entry is not None:
504 504 # backup the previous state
505 505 if entry[0] == b'm': # merge
506 506 size = -1
507 507 elif entry[0] == b'n' and entry[2] == -2: # other parent
508 508 size = -2
509 509 self._map.otherparentset.add(f)
510 510 self._updatedfiles.add(f)
511 511 self._map.removefile(f, oldstate, size)
512 512 if size == 0:
513 513 self._map.copymap.pop(f, None)
514 514
515 515 def merge(self, f):
516 516 '''Mark a file merged.'''
517 517 if self._pl[1] == nullid:
518 518 return self.normallookup(f)
519 519 return self.otherparent(f)
520 520
521 521 def drop(self, f):
522 522 '''Drop a file from the dirstate'''
523 523 oldstate = self[f]
524 524 if self._map.dropfile(f, oldstate):
525 525 self._dirty = True
526 526 self._updatedfiles.add(f)
527 527 self._map.copymap.pop(f, None)
528 528
529 529 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
530 530 if exists is None:
531 531 exists = os.path.lexists(os.path.join(self._root, path))
532 532 if not exists:
533 533 # Maybe a path component exists
534 534 if not ignoremissing and b'/' in path:
535 535 d, f = path.rsplit(b'/', 1)
536 536 d = self._normalize(d, False, ignoremissing, None)
537 537 folded = d + b"/" + f
538 538 else:
539 539 # No path components, preserve original case
540 540 folded = path
541 541 else:
542 542 # recursively normalize leading directory components
543 543 # against dirstate
544 544 if b'/' in normed:
545 545 d, f = normed.rsplit(b'/', 1)
546 546 d = self._normalize(d, False, ignoremissing, True)
547 547 r = self._root + b"/" + d
548 548 folded = d + b"/" + util.fspath(f, r)
549 549 else:
550 550 folded = util.fspath(normed, self._root)
551 551 storemap[normed] = folded
552 552
553 553 return folded
554 554
555 555 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
556 556 normed = util.normcase(path)
557 557 folded = self._map.filefoldmap.get(normed, None)
558 558 if folded is None:
559 559 if isknown:
560 560 folded = path
561 561 else:
562 562 folded = self._discoverpath(
563 563 path, normed, ignoremissing, exists, self._map.filefoldmap
564 564 )
565 565 return folded
566 566
567 567 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
568 568 normed = util.normcase(path)
569 569 folded = self._map.filefoldmap.get(normed, None)
570 570 if folded is None:
571 571 folded = self._map.dirfoldmap.get(normed, None)
572 572 if folded is None:
573 573 if isknown:
574 574 folded = path
575 575 else:
576 576 # store discovered result in dirfoldmap so that future
577 577 # normalizefile calls don't start matching directories
578 578 folded = self._discoverpath(
579 579 path, normed, ignoremissing, exists, self._map.dirfoldmap
580 580 )
581 581 return folded
582 582
583 583 def normalize(self, path, isknown=False, ignoremissing=False):
584 584 '''
585 585 normalize the case of a pathname when on a casefolding filesystem
586 586
587 587 isknown specifies whether the filename came from walking the
588 588 disk, to avoid extra filesystem access.
589 589
590 590 If ignoremissing is True, missing path are returned
591 591 unchanged. Otherwise, we try harder to normalize possibly
592 592 existing path components.
593 593
594 594 The normalized case is determined based on the following precedence:
595 595
596 596 - version of name already stored in the dirstate
597 597 - version of name stored on disk
598 598 - version provided via command arguments
599 599 '''
600 600
601 601 if self._checkcase:
602 602 return self._normalize(path, isknown, ignoremissing)
603 603 return path
604 604
605 605 def clear(self):
606 606 self._map.clear()
607 607 self._lastnormaltime = 0
608 608 self._updatedfiles.clear()
609 609 self._dirty = True
610 610
611 611 def rebuild(self, parent, allfiles, changedfiles=None):
612 612 if changedfiles is None:
613 613 # Rebuild entire dirstate
614 614 to_lookup = allfiles
615 615 to_drop = []
616 616 lastnormaltime = self._lastnormaltime
617 617 self.clear()
618 618 self._lastnormaltime = lastnormaltime
619 619 elif len(changedfiles) < 10:
620 620 # Avoid turning allfiles into a set, which can be expensive if it's
621 621 # large.
622 622 to_lookup = []
623 623 to_drop = []
624 624 for f in changedfiles:
625 625 if f in allfiles:
626 626 to_lookup.append(f)
627 627 else:
628 628 to_drop.append(f)
629 629 else:
630 630 changedfilesset = set(changedfiles)
631 631 to_lookup = changedfilesset & set(allfiles)
632 632 to_drop = changedfilesset - to_lookup
633 633
634 634 if self._origpl is None:
635 635 self._origpl = self._pl
636 636 self._map.setparents(parent, nullid)
637 637
638 638 for f in to_lookup:
639 639 self.normallookup(f)
640 640 for f in to_drop:
641 641 self.drop(f)
642 642
643 643 self._dirty = True
644 644
645 645 def identity(self):
646 646 '''Return identity of dirstate itself to detect changing in storage
647 647
648 648 If identity of previous dirstate is equal to this, writing
649 649 changes based on the former dirstate out can keep consistency.
650 650 '''
651 651 return self._map.identity
652 652
653 653 def write(self, tr):
654 654 if not self._dirty:
655 655 return
656 656
657 657 filename = self._filename
658 658 if tr:
659 659 # 'dirstate.write()' is not only for writing in-memory
660 660 # changes out, but also for dropping ambiguous timestamp.
661 661 # delayed writing re-raise "ambiguous timestamp issue".
662 662 # See also the wiki page below for detail:
663 663 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
664 664
665 665 # emulate dropping timestamp in 'parsers.pack_dirstate'
666 666 now = _getfsnow(self._opener)
667 667 self._map.clearambiguoustimes(self._updatedfiles, now)
668 668
669 669 # emulate that all 'dirstate.normal' results are written out
670 670 self._lastnormaltime = 0
671 671 self._updatedfiles.clear()
672 672
673 673 # delay writing in-memory changes out
674 674 tr.addfilegenerator(
675 675 b'dirstate',
676 676 (self._filename,),
677 677 self._writedirstate,
678 678 location=b'plain',
679 679 )
680 680 return
681 681
682 682 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
683 683 self._writedirstate(st)
684 684
685 685 def addparentchangecallback(self, category, callback):
686 686 """add a callback to be called when the wd parents are changed
687 687
688 688 Callback will be called with the following arguments:
689 689 dirstate, (oldp1, oldp2), (newp1, newp2)
690 690
691 691 Category is a unique identifier to allow overwriting an old callback
692 692 with a newer callback.
693 693 """
694 694 self._plchangecallbacks[category] = callback
695 695
696 696 def _writedirstate(self, st):
697 697 # notify callbacks about parents change
698 698 if self._origpl is not None and self._origpl != self._pl:
699 699 for c, callback in sorted(
700 700 pycompat.iteritems(self._plchangecallbacks)
701 701 ):
702 702 callback(self, self._origpl, self._pl)
703 703 self._origpl = None
704 704 # use the modification time of the newly created temporary file as the
705 705 # filesystem's notion of 'now'
706 706 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
707 707
708 708 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
709 709 # timestamp of each entries in dirstate, because of 'now > mtime'
710 710 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
711 711 if delaywrite > 0:
712 712 # do we have any files to delay for?
713 713 for f, e in pycompat.iteritems(self._map):
714 714 if e[0] == b'n' and e[3] == now:
715 715 import time # to avoid useless import
716 716
717 717 # rather than sleep n seconds, sleep until the next
718 718 # multiple of n seconds
719 719 clock = time.time()
720 720 start = int(clock) - (int(clock) % delaywrite)
721 721 end = start + delaywrite
722 722 time.sleep(end - clock)
723 723 now = end # trust our estimate that the end is near now
724 724 break
725 725
726 726 self._map.write(st, now)
727 727 self._lastnormaltime = 0
728 728 self._dirty = False
729 729
730 730 def _dirignore(self, f):
731 731 if self._ignore(f):
732 732 return True
733 733 for p in pathutil.finddirs(f):
734 734 if self._ignore(p):
735 735 return True
736 736 return False
737 737
738 738 def _ignorefiles(self):
739 739 files = []
740 740 if os.path.exists(self._join(b'.hgignore')):
741 741 files.append(self._join(b'.hgignore'))
742 742 for name, path in self._ui.configitems(b"ui"):
743 743 if name == b'ignore' or name.startswith(b'ignore.'):
744 744 # we need to use os.path.join here rather than self._join
745 745 # because path is arbitrary and user-specified
746 746 files.append(os.path.join(self._rootdir, util.expandpath(path)))
747 747 return files
748 748
749 749 def _ignorefileandline(self, f):
750 750 files = collections.deque(self._ignorefiles())
751 751 visited = set()
752 752 while files:
753 753 i = files.popleft()
754 754 patterns = matchmod.readpatternfile(
755 755 i, self._ui.warn, sourceinfo=True
756 756 )
757 757 for pattern, lineno, line in patterns:
758 758 kind, p = matchmod._patsplit(pattern, b'glob')
759 759 if kind == b"subinclude":
760 760 if p not in visited:
761 761 files.append(p)
762 762 continue
763 763 m = matchmod.match(
764 764 self._root, b'', [], [pattern], warn=self._ui.warn
765 765 )
766 766 if m(f):
767 767 return (i, lineno, line)
768 768 visited.add(i)
769 769 return (None, -1, b"")
770 770
771 771 def _walkexplicit(self, match, subrepos):
772 772 '''Get stat data about the files explicitly specified by match.
773 773
774 774 Return a triple (results, dirsfound, dirsnotfound).
775 775 - results is a mapping from filename to stat result. It also contains
776 776 listings mapping subrepos and .hg to None.
777 777 - dirsfound is a list of files found to be directories.
778 778 - dirsnotfound is a list of files that the dirstate thinks are
779 779 directories and that were not found.'''
780 780
781 781 def badtype(mode):
782 782 kind = _(b'unknown')
783 783 if stat.S_ISCHR(mode):
784 784 kind = _(b'character device')
785 785 elif stat.S_ISBLK(mode):
786 786 kind = _(b'block device')
787 787 elif stat.S_ISFIFO(mode):
788 788 kind = _(b'fifo')
789 789 elif stat.S_ISSOCK(mode):
790 790 kind = _(b'socket')
791 791 elif stat.S_ISDIR(mode):
792 792 kind = _(b'directory')
793 793 return _(b'unsupported file type (type is %s)') % kind
794 794
795 795 badfn = match.bad
796 796 dmap = self._map
797 797 lstat = os.lstat
798 798 getkind = stat.S_IFMT
799 799 dirkind = stat.S_IFDIR
800 800 regkind = stat.S_IFREG
801 801 lnkkind = stat.S_IFLNK
802 802 join = self._join
803 803 dirsfound = []
804 804 foundadd = dirsfound.append
805 805 dirsnotfound = []
806 806 notfoundadd = dirsnotfound.append
807 807
808 808 if not match.isexact() and self._checkcase:
809 809 normalize = self._normalize
810 810 else:
811 811 normalize = None
812 812
813 813 files = sorted(match.files())
814 814 subrepos.sort()
815 815 i, j = 0, 0
816 816 while i < len(files) and j < len(subrepos):
817 817 subpath = subrepos[j] + b"/"
818 818 if files[i] < subpath:
819 819 i += 1
820 820 continue
821 821 while i < len(files) and files[i].startswith(subpath):
822 822 del files[i]
823 823 j += 1
824 824
825 825 if not files or b'' in files:
826 826 files = [b'']
827 827 # constructing the foldmap is expensive, so don't do it for the
828 828 # common case where files is ['']
829 829 normalize = None
830 830 results = dict.fromkeys(subrepos)
831 831 results[b'.hg'] = None
832 832
833 833 for ff in files:
834 834 if normalize:
835 835 nf = normalize(ff, False, True)
836 836 else:
837 837 nf = ff
838 838 if nf in results:
839 839 continue
840 840
841 841 try:
842 842 st = lstat(join(nf))
843 843 kind = getkind(st.st_mode)
844 844 if kind == dirkind:
845 845 if nf in dmap:
846 846 # file replaced by dir on disk but still in dirstate
847 847 results[nf] = None
848 848 foundadd((nf, ff))
849 849 elif kind == regkind or kind == lnkkind:
850 850 results[nf] = st
851 851 else:
852 852 badfn(ff, badtype(kind))
853 853 if nf in dmap:
854 854 results[nf] = None
855 855 except OSError as inst: # nf not found on disk - it is dirstate only
856 856 if nf in dmap: # does it exactly match a missing file?
857 857 results[nf] = None
858 858 else: # does it match a missing directory?
859 859 if self._map.hasdir(nf):
860 860 notfoundadd(nf)
861 861 else:
862 862 badfn(ff, encoding.strtolocal(inst.strerror))
863 863
864 864 # match.files() may contain explicitly-specified paths that shouldn't
865 865 # be taken; drop them from the list of files found. dirsfound/notfound
866 866 # aren't filtered here because they will be tested later.
867 867 if match.anypats():
868 868 for f in list(results):
869 869 if f == b'.hg' or f in subrepos:
870 870 # keep sentinel to disable further out-of-repo walks
871 871 continue
872 872 if not match(f):
873 873 del results[f]
874 874
875 875 # Case insensitive filesystems cannot rely on lstat() failing to detect
876 876 # a case-only rename. Prune the stat object for any file that does not
877 877 # match the case in the filesystem, if there are multiple files that
878 878 # normalize to the same path.
879 879 if match.isexact() and self._checkcase:
880 880 normed = {}
881 881
882 882 for f, st in pycompat.iteritems(results):
883 883 if st is None:
884 884 continue
885 885
886 886 nc = util.normcase(f)
887 887 paths = normed.get(nc)
888 888
889 889 if paths is None:
890 890 paths = set()
891 891 normed[nc] = paths
892 892
893 893 paths.add(f)
894 894
895 895 for norm, paths in pycompat.iteritems(normed):
896 896 if len(paths) > 1:
897 897 for path in paths:
898 898 folded = self._discoverpath(
899 899 path, norm, True, None, self._map.dirfoldmap
900 900 )
901 901 if path != folded:
902 902 results[path] = None
903 903
904 904 return results, dirsfound, dirsnotfound
905 905
906 906 def walk(self, match, subrepos, unknown, ignored, full=True):
907 907 '''
908 908 Walk recursively through the directory tree, finding all files
909 909 matched by match.
910 910
911 911 If full is False, maybe skip some known-clean files.
912 912
913 913 Return a dict mapping filename to stat-like object (either
914 914 mercurial.osutil.stat instance or return value of os.stat()).
915 915
916 916 '''
917 917 # full is a flag that extensions that hook into walk can use -- this
918 918 # implementation doesn't use it at all. This satisfies the contract
919 919 # because we only guarantee a "maybe".
920 920
921 921 if ignored:
922 922 ignore = util.never
923 923 dirignore = util.never
924 924 elif unknown:
925 925 ignore = self._ignore
926 926 dirignore = self._dirignore
927 927 else:
928 928 # if not unknown and not ignored, drop dir recursion and step 2
929 929 ignore = util.always
930 930 dirignore = util.always
931 931
932 932 matchfn = match.matchfn
933 933 matchalways = match.always()
934 934 matchtdir = match.traversedir
935 935 dmap = self._map
936 936 listdir = util.listdir
937 937 lstat = os.lstat
938 938 dirkind = stat.S_IFDIR
939 939 regkind = stat.S_IFREG
940 940 lnkkind = stat.S_IFLNK
941 941 join = self._join
942 942
943 943 exact = skipstep3 = False
944 944 if match.isexact(): # match.exact
945 945 exact = True
946 946 dirignore = util.always # skip step 2
947 947 elif match.prefix(): # match.match, no patterns
948 948 skipstep3 = True
949 949
950 950 if not exact and self._checkcase:
951 951 normalize = self._normalize
952 952 normalizefile = self._normalizefile
953 953 skipstep3 = False
954 954 else:
955 955 normalize = self._normalize
956 956 normalizefile = None
957 957
958 958 # step 1: find all explicit files
959 959 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
960 960 if matchtdir:
961 961 for d in work:
962 962 matchtdir(d[0])
963 963 for d in dirsnotfound:
964 964 matchtdir(d)
965 965
966 966 skipstep3 = skipstep3 and not (work or dirsnotfound)
967 967 work = [d for d in work if not dirignore(d[0])]
968 968
969 969 # step 2: visit subdirectories
970 970 def traverse(work, alreadynormed):
971 971 wadd = work.append
972 972 while work:
973 973 tracing.counter('dirstate.walk work', len(work))
974 974 nd = work.pop()
975 975 visitentries = match.visitchildrenset(nd)
976 976 if not visitentries:
977 977 continue
978 978 if visitentries == b'this' or visitentries == b'all':
979 979 visitentries = None
980 980 skip = None
981 981 if nd != b'':
982 982 skip = b'.hg'
983 983 try:
984 984 with tracing.log('dirstate.walk.traverse listdir %s', nd):
985 985 entries = listdir(join(nd), stat=True, skip=skip)
986 986 except OSError as inst:
987 987 if inst.errno in (errno.EACCES, errno.ENOENT):
988 988 match.bad(
989 989 self.pathto(nd), encoding.strtolocal(inst.strerror)
990 990 )
991 991 continue
992 992 raise
993 993 for f, kind, st in entries:
994 994 # Some matchers may return files in the visitentries set,
995 995 # instead of 'this', if the matcher explicitly mentions them
996 996 # and is not an exactmatcher. This is acceptable; we do not
997 997 # make any hard assumptions about file-or-directory below
998 998 # based on the presence of `f` in visitentries. If
999 999 # visitchildrenset returned a set, we can always skip the
1000 1000 # entries *not* in the set it provided regardless of whether
1001 1001 # they're actually a file or a directory.
1002 1002 if visitentries and f not in visitentries:
1003 1003 continue
1004 1004 if normalizefile:
1005 1005 # even though f might be a directory, we're only
1006 1006 # interested in comparing it to files currently in the
1007 1007 # dmap -- therefore normalizefile is enough
1008 1008 nf = normalizefile(
1009 1009 nd and (nd + b"/" + f) or f, True, True
1010 1010 )
1011 1011 else:
1012 1012 nf = nd and (nd + b"/" + f) or f
1013 1013 if nf not in results:
1014 1014 if kind == dirkind:
1015 1015 if not ignore(nf):
1016 1016 if matchtdir:
1017 1017 matchtdir(nf)
1018 1018 wadd(nf)
1019 1019 if nf in dmap and (matchalways or matchfn(nf)):
1020 1020 results[nf] = None
1021 1021 elif kind == regkind or kind == lnkkind:
1022 1022 if nf in dmap:
1023 1023 if matchalways or matchfn(nf):
1024 1024 results[nf] = st
1025 1025 elif (matchalways or matchfn(nf)) and not ignore(
1026 1026 nf
1027 1027 ):
1028 1028 # unknown file -- normalize if necessary
1029 1029 if not alreadynormed:
1030 1030 nf = normalize(nf, False, True)
1031 1031 results[nf] = st
1032 1032 elif nf in dmap and (matchalways or matchfn(nf)):
1033 1033 results[nf] = None
1034 1034
1035 1035 for nd, d in work:
1036 1036 # alreadynormed means that processwork doesn't have to do any
1037 1037 # expensive directory normalization
1038 1038 alreadynormed = not normalize or nd == d
1039 1039 traverse([d], alreadynormed)
1040 1040
1041 1041 for s in subrepos:
1042 1042 del results[s]
1043 1043 del results[b'.hg']
1044 1044
1045 1045 # step 3: visit remaining files from dmap
1046 1046 if not skipstep3 and not exact:
1047 1047 # If a dmap file is not in results yet, it was either
1048 1048 # a) not matching matchfn b) ignored, c) missing, or d) under a
1049 1049 # symlink directory.
1050 1050 if not results and matchalways:
1051 1051 visit = [f for f in dmap]
1052 1052 else:
1053 1053 visit = [f for f in dmap if f not in results and matchfn(f)]
1054 1054 visit.sort()
1055 1055
1056 1056 if unknown:
1057 1057 # unknown == True means we walked all dirs under the roots
1058 1058 # that wasn't ignored, and everything that matched was stat'ed
1059 1059 # and is already in results.
1060 1060 # The rest must thus be ignored or under a symlink.
1061 1061 audit_path = pathutil.pathauditor(self._root, cached=True)
1062 1062
1063 1063 for nf in iter(visit):
1064 1064 # If a stat for the same file was already added with a
1065 1065 # different case, don't add one for this, since that would
1066 1066 # make it appear as if the file exists under both names
1067 1067 # on disk.
1068 1068 if (
1069 1069 normalizefile
1070 1070 and normalizefile(nf, True, True) in results
1071 1071 ):
1072 1072 results[nf] = None
1073 1073 # Report ignored items in the dmap as long as they are not
1074 1074 # under a symlink directory.
1075 1075 elif audit_path.check(nf):
1076 1076 try:
1077 1077 results[nf] = lstat(join(nf))
1078 1078 # file was just ignored, no links, and exists
1079 1079 except OSError:
1080 1080 # file doesn't exist
1081 1081 results[nf] = None
1082 1082 else:
1083 1083 # It's either missing or under a symlink directory
1084 1084 # which we in this case report as missing
1085 1085 results[nf] = None
1086 1086 else:
1087 1087 # We may not have walked the full directory tree above,
1088 1088 # so stat and check everything we missed.
1089 1089 iv = iter(visit)
1090 1090 for st in util.statfiles([join(i) for i in visit]):
1091 1091 results[next(iv)] = st
1092 1092 return results
1093 1093
1094 1094 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1095 1095 # Force Rayon (Rust parallelism library) to respect the number of
1096 1096 # workers. This is a temporary workaround until Rust code knows
1097 1097 # how to read the config file.
1098 1098 numcpus = self._ui.configint(b"worker", b"numcpus")
1099 1099 if numcpus is not None:
1100 1100 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1101 1101
1102 1102 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1103 1103 if not workers_enabled:
1104 1104 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1105 1105
1106 1106 (
1107 1107 lookup,
1108 1108 modified,
1109 1109 added,
1110 1110 removed,
1111 1111 deleted,
1112 1112 clean,
1113 1113 ignored,
1114 1114 unknown,
1115 1115 warnings,
1116 1116 bad,
1117 1117 traversed,
1118 1118 ) = rustmod.status(
1119 1119 self._map._rustmap,
1120 1120 matcher,
1121 1121 self._rootdir,
1122 1122 self._ignorefiles(),
1123 1123 self._checkexec,
1124 1124 self._lastnormaltime,
1125 1125 bool(list_clean),
1126 1126 bool(list_ignored),
1127 1127 bool(list_unknown),
1128 1128 bool(matcher.traversedir),
1129 1129 )
1130 1130
1131 1131 if matcher.traversedir:
1132 1132 for dir in traversed:
1133 1133 matcher.traversedir(dir)
1134 1134
1135 1135 if self._ui.warn:
1136 1136 for item in warnings:
1137 1137 if isinstance(item, tuple):
1138 1138 file_path, syntax = item
1139 1139 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1140 1140 file_path,
1141 1141 syntax,
1142 1142 )
1143 1143 self._ui.warn(msg)
1144 1144 else:
1145 1145 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1146 1146 self._ui.warn(
1147 1147 msg
1148 1148 % (
1149 1149 pathutil.canonpath(
1150 1150 self._rootdir, self._rootdir, item
1151 1151 ),
1152 1152 b"No such file or directory",
1153 1153 )
1154 1154 )
1155 1155
1156 1156 for (fn, message) in bad:
1157 1157 matcher.bad(fn, encoding.strtolocal(message))
1158 1158
1159 1159 status = scmutil.status(
1160 1160 modified=modified,
1161 1161 added=added,
1162 1162 removed=removed,
1163 1163 deleted=deleted,
1164 1164 unknown=unknown,
1165 1165 ignored=ignored,
1166 1166 clean=clean,
1167 1167 )
1168 1168 return (lookup, status)
1169 1169
1170 1170 def status(self, match, subrepos, ignored, clean, unknown):
1171 1171 '''Determine the status of the working copy relative to the
1172 1172 dirstate and return a pair of (unsure, status), where status is of type
1173 1173 scmutil.status and:
1174 1174
1175 1175 unsure:
1176 1176 files that might have been modified since the dirstate was
1177 1177 written, but need to be read to be sure (size is the same
1178 1178 but mtime differs)
1179 1179 status.modified:
1180 1180 files that have definitely been modified since the dirstate
1181 1181 was written (different size or mode)
1182 1182 status.clean:
1183 1183 files that have definitely not been modified since the
1184 1184 dirstate was written
1185 1185 '''
1186 1186 listignored, listclean, listunknown = ignored, clean, unknown
1187 1187 lookup, modified, added, unknown, ignored = [], [], [], [], []
1188 1188 removed, deleted, clean = [], [], []
1189 1189
1190 1190 dmap = self._map
1191 1191 dmap.preload()
1192 1192
1193 1193 use_rust = True
1194 1194
1195 1195 allowed_matchers = (
1196 1196 matchmod.alwaysmatcher,
1197 1197 matchmod.exactmatcher,
1198 1198 matchmod.includematcher,
1199 1199 )
1200 1200
1201 1201 if rustmod is None:
1202 1202 use_rust = False
1203 1203 elif self._checkcase:
1204 1204 # Case-insensitive filesystems are not handled yet
1205 1205 use_rust = False
1206 1206 elif subrepos:
1207 1207 use_rust = False
1208 1208 elif sparse.enabled:
1209 1209 use_rust = False
1210 1210 elif not isinstance(match, allowed_matchers):
1211 1211 # Some matchers have yet to be implemented
1212 1212 use_rust = False
1213 1213
1214 1214 if use_rust:
1215 1215 try:
1216 1216 return self._rust_status(
1217 1217 match, listclean, listignored, listunknown
1218 1218 )
1219 1219 except rustmod.FallbackError:
1220 1220 pass
1221 1221
1222 1222 def noop(f):
1223 1223 pass
1224 1224
1225 1225 dcontains = dmap.__contains__
1226 1226 dget = dmap.__getitem__
1227 1227 ladd = lookup.append # aka "unsure"
1228 1228 madd = modified.append
1229 1229 aadd = added.append
1230 1230 uadd = unknown.append if listunknown else noop
1231 1231 iadd = ignored.append if listignored else noop
1232 1232 radd = removed.append
1233 1233 dadd = deleted.append
1234 1234 cadd = clean.append if listclean else noop
1235 1235 mexact = match.exact
1236 1236 dirignore = self._dirignore
1237 1237 checkexec = self._checkexec
1238 1238 copymap = self._map.copymap
1239 1239 lastnormaltime = self._lastnormaltime
1240 1240
1241 1241 # We need to do full walks when either
1242 1242 # - we're listing all clean files, or
1243 1243 # - match.traversedir does something, because match.traversedir should
1244 1244 # be called for every dir in the working dir
1245 1245 full = listclean or match.traversedir is not None
1246 1246 for fn, st in pycompat.iteritems(
1247 1247 self.walk(match, subrepos, listunknown, listignored, full=full)
1248 1248 ):
1249 1249 if not dcontains(fn):
1250 1250 if (listignored or mexact(fn)) and dirignore(fn):
1251 1251 if listignored:
1252 1252 iadd(fn)
1253 1253 else:
1254 1254 uadd(fn)
1255 1255 continue
1256 1256
1257 1257 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1258 1258 # written like that for performance reasons. dmap[fn] is not a
1259 1259 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1260 1260 # opcode has fast paths when the value to be unpacked is a tuple or
1261 1261 # a list, but falls back to creating a full-fledged iterator in
1262 1262 # general. That is much slower than simply accessing and storing the
1263 1263 # tuple members one by one.
1264 1264 t = dget(fn)
1265 1265 state = t[0]
1266 1266 mode = t[1]
1267 1267 size = t[2]
1268 1268 time = t[3]
1269 1269
1270 1270 if not st and state in b"nma":
1271 1271 dadd(fn)
1272 1272 elif state == b'n':
1273 1273 if (
1274 1274 size >= 0
1275 1275 and (
1276 1276 (size != st.st_size and size != st.st_size & _rangemask)
1277 1277 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1278 1278 )
1279 1279 or size == -2 # other parent
1280 1280 or fn in copymap
1281 1281 ):
1282 1282 madd(fn)
1283 1283 elif (
1284 1284 time != st[stat.ST_MTIME]
1285 1285 and time != st[stat.ST_MTIME] & _rangemask
1286 1286 ):
1287 1287 ladd(fn)
1288 1288 elif st[stat.ST_MTIME] == lastnormaltime:
1289 1289 # fn may have just been marked as normal and it may have
1290 1290 # changed in the same second without changing its size.
1291 1291 # This can happen if we quickly do multiple commits.
1292 1292 # Force lookup, so we don't miss such a racy file change.
1293 1293 ladd(fn)
1294 1294 elif listclean:
1295 1295 cadd(fn)
1296 1296 elif state == b'm':
1297 1297 madd(fn)
1298 1298 elif state == b'a':
1299 1299 aadd(fn)
1300 1300 elif state == b'r':
1301 1301 radd(fn)
1302 1302 status = scmutil.status(
1303 1303 modified, added, removed, deleted, unknown, ignored, clean
1304 1304 )
1305 1305 return (lookup, status)
1306 1306
1307 1307 def matches(self, match):
1308 1308 '''
1309 1309 return files in the dirstate (in whatever state) filtered by match
1310 1310 '''
1311 1311 dmap = self._map
1312 1312 if rustmod is not None:
1313 1313 dmap = self._map._rustmap
1314 1314
1315 1315 if match.always():
1316 1316 return dmap.keys()
1317 1317 files = match.files()
1318 1318 if match.isexact():
1319 1319 # fast path -- filter the other way around, since typically files is
1320 1320 # much smaller than dmap
1321 1321 return [f for f in files if f in dmap]
1322 1322 if match.prefix() and all(fn in dmap for fn in files):
1323 1323 # fast path -- all the values are known to be files, so just return
1324 1324 # that
1325 1325 return list(files)
1326 1326 return [f for f in dmap if match(f)]
1327 1327
1328 1328 def _actualfilename(self, tr):
1329 1329 if tr:
1330 1330 return self._pendingfilename
1331 1331 else:
1332 1332 return self._filename
1333 1333
1334 1334 def savebackup(self, tr, backupname):
1335 1335 '''Save current dirstate into backup file'''
1336 1336 filename = self._actualfilename(tr)
1337 1337 assert backupname != filename
1338 1338
1339 1339 # use '_writedirstate' instead of 'write' to write changes certainly,
1340 1340 # because the latter omits writing out if transaction is running.
1341 1341 # output file will be used to create backup of dirstate at this point.
1342 1342 if self._dirty or not self._opener.exists(filename):
1343 1343 self._writedirstate(
1344 1344 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1345 1345 )
1346 1346
1347 1347 if tr:
1348 1348 # ensure that subsequent tr.writepending returns True for
1349 1349 # changes written out above, even if dirstate is never
1350 1350 # changed after this
1351 1351 tr.addfilegenerator(
1352 1352 b'dirstate',
1353 1353 (self._filename,),
1354 1354 self._writedirstate,
1355 1355 location=b'plain',
1356 1356 )
1357 1357
1358 1358 # ensure that pending file written above is unlinked at
1359 1359 # failure, even if tr.writepending isn't invoked until the
1360 1360 # end of this transaction
1361 1361 tr.registertmp(filename, location=b'plain')
1362 1362
1363 1363 self._opener.tryunlink(backupname)
1364 1364 # hardlink backup is okay because _writedirstate is always called
1365 1365 # with an "atomictemp=True" file.
1366 1366 util.copyfile(
1367 1367 self._opener.join(filename),
1368 1368 self._opener.join(backupname),
1369 1369 hardlink=True,
1370 1370 )
1371 1371
1372 1372 def restorebackup(self, tr, backupname):
1373 1373 '''Restore dirstate by backup file'''
1374 1374 # this "invalidate()" prevents "wlock.release()" from writing
1375 1375 # changes of dirstate out after restoring from backup file
1376 1376 self.invalidate()
1377 1377 filename = self._actualfilename(tr)
1378 1378 o = self._opener
1379 1379 if util.samefile(o.join(backupname), o.join(filename)):
1380 1380 o.unlink(backupname)
1381 1381 else:
1382 1382 o.rename(backupname, filename, checkambig=True)
1383 1383
1384 1384 def clearbackup(self, tr, backupname):
1385 1385 '''Clear backup file'''
1386 1386 self._opener.unlink(backupname)
1387 1387
1388 1388
1389 1389 class dirstatemap(object):
1390 1390 """Map encapsulating the dirstate's contents.
1391 1391
1392 1392 The dirstate contains the following state:
1393 1393
1394 1394 - `identity` is the identity of the dirstate file, which can be used to
1395 1395 detect when changes have occurred to the dirstate file.
1396 1396
1397 1397 - `parents` is a pair containing the parents of the working copy. The
1398 1398 parents are updated by calling `setparents`.
1399 1399
1400 1400 - the state map maps filenames to tuples of (state, mode, size, mtime),
1401 1401 where state is a single character representing 'normal', 'added',
1402 1402 'removed', or 'merged'. It is read by treating the dirstate as a
1403 1403 dict. File state is updated by calling the `addfile`, `removefile` and
1404 1404 `dropfile` methods.
1405 1405
1406 1406 - `copymap` maps destination filenames to their source filename.
1407 1407
1408 1408 The dirstate also provides the following views onto the state:
1409 1409
1410 1410 - `nonnormalset` is a set of the filenames that have state other
1411 1411 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1412 1412
1413 1413 - `otherparentset` is a set of the filenames that are marked as coming
1414 1414 from the second parent when the dirstate is currently being merged.
1415 1415
1416 1416 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1417 1417 form that they appear as in the dirstate.
1418 1418
1419 1419 - `dirfoldmap` is a dict mapping normalized directory names to the
1420 1420 denormalized form that they appear as in the dirstate.
1421 1421 """
1422 1422
1423 1423 def __init__(self, ui, opener, root):
1424 1424 self._ui = ui
1425 1425 self._opener = opener
1426 1426 self._root = root
1427 1427 self._filename = b'dirstate'
1428 self._nodelen = 20
1428 1429
1429 1430 self._parents = None
1430 1431 self._dirtyparents = False
1431 1432
1432 1433 # for consistent view between _pl() and _read() invocations
1433 1434 self._pendingmode = None
1434 1435
1435 1436 @propertycache
1436 1437 def _map(self):
1437 1438 self._map = {}
1438 1439 self.read()
1439 1440 return self._map
1440 1441
1441 1442 @propertycache
1442 1443 def copymap(self):
1443 1444 self.copymap = {}
1444 1445 self._map
1445 1446 return self.copymap
1446 1447
1447 1448 def clear(self):
1448 1449 self._map.clear()
1449 1450 self.copymap.clear()
1450 1451 self.setparents(nullid, nullid)
1451 1452 util.clearcachedproperty(self, b"_dirs")
1452 1453 util.clearcachedproperty(self, b"_alldirs")
1453 1454 util.clearcachedproperty(self, b"filefoldmap")
1454 1455 util.clearcachedproperty(self, b"dirfoldmap")
1455 1456 util.clearcachedproperty(self, b"nonnormalset")
1456 1457 util.clearcachedproperty(self, b"otherparentset")
1457 1458
1458 1459 def items(self):
1459 1460 return pycompat.iteritems(self._map)
1460 1461
1461 1462 # forward for python2,3 compat
1462 1463 iteritems = items
1463 1464
1464 1465 def __len__(self):
1465 1466 return len(self._map)
1466 1467
1467 1468 def __iter__(self):
1468 1469 return iter(self._map)
1469 1470
1470 1471 def get(self, key, default=None):
1471 1472 return self._map.get(key, default)
1472 1473
1473 1474 def __contains__(self, key):
1474 1475 return key in self._map
1475 1476
1476 1477 def __getitem__(self, key):
1477 1478 return self._map[key]
1478 1479
1479 1480 def keys(self):
1480 1481 return self._map.keys()
1481 1482
1482 1483 def preload(self):
1483 1484 """Loads the underlying data, if it's not already loaded"""
1484 1485 self._map
1485 1486
1486 1487 def addfile(self, f, oldstate, state, mode, size, mtime):
1487 1488 """Add a tracked file to the dirstate."""
1488 1489 if oldstate in b"?r" and "_dirs" in self.__dict__:
1489 1490 self._dirs.addpath(f)
1490 1491 if oldstate == b"?" and "_alldirs" in self.__dict__:
1491 1492 self._alldirs.addpath(f)
1492 1493 self._map[f] = dirstatetuple(state, mode, size, mtime)
1493 1494 if state != b'n' or mtime == -1:
1494 1495 self.nonnormalset.add(f)
1495 1496 if size == -2:
1496 1497 self.otherparentset.add(f)
1497 1498
1498 1499 def removefile(self, f, oldstate, size):
1499 1500 """
1500 1501 Mark a file as removed in the dirstate.
1501 1502
1502 1503 The `size` parameter is used to store sentinel values that indicate
1503 1504 the file's previous state. In the future, we should refactor this
1504 1505 to be more explicit about what that state is.
1505 1506 """
1506 1507 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1507 1508 self._dirs.delpath(f)
1508 1509 if oldstate == b"?" and "_alldirs" in self.__dict__:
1509 1510 self._alldirs.addpath(f)
1510 1511 if "filefoldmap" in self.__dict__:
1511 1512 normed = util.normcase(f)
1512 1513 self.filefoldmap.pop(normed, None)
1513 1514 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1514 1515 self.nonnormalset.add(f)
1515 1516
1516 1517 def dropfile(self, f, oldstate):
1517 1518 """
1518 1519 Remove a file from the dirstate. Returns True if the file was
1519 1520 previously recorded.
1520 1521 """
1521 1522 exists = self._map.pop(f, None) is not None
1522 1523 if exists:
1523 1524 if oldstate != b"r" and "_dirs" in self.__dict__:
1524 1525 self._dirs.delpath(f)
1525 1526 if "_alldirs" in self.__dict__:
1526 1527 self._alldirs.delpath(f)
1527 1528 if "filefoldmap" in self.__dict__:
1528 1529 normed = util.normcase(f)
1529 1530 self.filefoldmap.pop(normed, None)
1530 1531 self.nonnormalset.discard(f)
1531 1532 return exists
1532 1533
1533 1534 def clearambiguoustimes(self, files, now):
1534 1535 for f in files:
1535 1536 e = self.get(f)
1536 1537 if e is not None and e[0] == b'n' and e[3] == now:
1537 1538 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1538 1539 self.nonnormalset.add(f)
1539 1540
1540 1541 def nonnormalentries(self):
1541 1542 '''Compute the nonnormal dirstate entries from the dmap'''
1542 1543 try:
1543 1544 return parsers.nonnormalotherparententries(self._map)
1544 1545 except AttributeError:
1545 1546 nonnorm = set()
1546 1547 otherparent = set()
1547 1548 for fname, e in pycompat.iteritems(self._map):
1548 1549 if e[0] != b'n' or e[3] == -1:
1549 1550 nonnorm.add(fname)
1550 1551 if e[0] == b'n' and e[2] == -2:
1551 1552 otherparent.add(fname)
1552 1553 return nonnorm, otherparent
1553 1554
1554 1555 @propertycache
1555 1556 def filefoldmap(self):
1556 1557 """Returns a dictionary mapping normalized case paths to their
1557 1558 non-normalized versions.
1558 1559 """
1559 1560 try:
1560 1561 makefilefoldmap = parsers.make_file_foldmap
1561 1562 except AttributeError:
1562 1563 pass
1563 1564 else:
1564 1565 return makefilefoldmap(
1565 1566 self._map, util.normcasespec, util.normcasefallback
1566 1567 )
1567 1568
1568 1569 f = {}
1569 1570 normcase = util.normcase
1570 1571 for name, s in pycompat.iteritems(self._map):
1571 1572 if s[0] != b'r':
1572 1573 f[normcase(name)] = name
1573 1574 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1574 1575 return f
1575 1576
1576 1577 def hastrackeddir(self, d):
1577 1578 """
1578 1579 Returns True if the dirstate contains a tracked (not removed) file
1579 1580 in this directory.
1580 1581 """
1581 1582 return d in self._dirs
1582 1583
1583 1584 def hasdir(self, d):
1584 1585 """
1585 1586 Returns True if the dirstate contains a file (tracked or removed)
1586 1587 in this directory.
1587 1588 """
1588 1589 return d in self._alldirs
1589 1590
1590 1591 @propertycache
1591 1592 def _dirs(self):
1592 1593 return pathutil.dirs(self._map, b'r')
1593 1594
1594 1595 @propertycache
1595 1596 def _alldirs(self):
1596 1597 return pathutil.dirs(self._map)
1597 1598
1598 1599 def _opendirstatefile(self):
1599 1600 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1600 1601 if self._pendingmode is not None and self._pendingmode != mode:
1601 1602 fp.close()
1602 1603 raise error.Abort(
1603 1604 _(b'working directory state may be changed parallelly')
1604 1605 )
1605 1606 self._pendingmode = mode
1606 1607 return fp
1607 1608
1608 1609 def parents(self):
1609 1610 if not self._parents:
1610 1611 try:
1611 1612 fp = self._opendirstatefile()
1612 st = fp.read(40)
1613 st = fp.read(2 * self._nodelen)
1613 1614 fp.close()
1614 1615 except IOError as err:
1615 1616 if err.errno != errno.ENOENT:
1616 1617 raise
1617 1618 # File doesn't exist, so the current state is empty
1618 1619 st = b''
1619 1620
1620 1621 l = len(st)
1621 if l == 40:
1622 self._parents = (st[:20], st[20:40])
1622 if l == self._nodelen * 2:
1623 self._parents = (
1624 st[: self._nodelen],
1625 st[self._nodelen : 2 * self._nodelen],
1626 )
1623 1627 elif l == 0:
1624 1628 self._parents = (nullid, nullid)
1625 1629 else:
1626 1630 raise error.Abort(
1627 1631 _(b'working directory state appears damaged!')
1628 1632 )
1629 1633
1630 1634 return self._parents
1631 1635
1632 1636 def setparents(self, p1, p2):
1633 1637 self._parents = (p1, p2)
1634 1638 self._dirtyparents = True
1635 1639
1636 1640 def read(self):
1637 1641 # ignore HG_PENDING because identity is used only for writing
1638 1642 self.identity = util.filestat.frompath(
1639 1643 self._opener.join(self._filename)
1640 1644 )
1641 1645
1642 1646 try:
1643 1647 fp = self._opendirstatefile()
1644 1648 try:
1645 1649 st = fp.read()
1646 1650 finally:
1647 1651 fp.close()
1648 1652 except IOError as err:
1649 1653 if err.errno != errno.ENOENT:
1650 1654 raise
1651 1655 return
1652 1656 if not st:
1653 1657 return
1654 1658
1655 1659 if util.safehasattr(parsers, b'dict_new_presized'):
1656 1660 # Make an estimate of the number of files in the dirstate based on
1657 1661 # its size. From a linear regression on a set of real-world repos,
1658 # all over 10,000 files, the size of a dirstate entry is 85
1659 # bytes. The cost of resizing is significantly higher than the cost
1662 # all over 10,000 files, the size of a dirstate entry is 2 nodes
1663 # plus 45 bytes. The cost of resizing is significantly higher than the cost
1660 1664 # of filling in a larger presized dict, so subtract 20% from the
1661 1665 # size.
1662 1666 #
1663 1667 # This heuristic is imperfect in many ways, so in a future dirstate
1664 1668 # format update it makes sense to just record the number of entries
1665 1669 # on write.
1666 self._map = parsers.dict_new_presized(len(st) // 71)
1670 self._map = parsers.dict_new_presized(
1671 len(st) // ((2 * self._nodelen + 45) * 4 // 5)
1672 )
1667 1673
1668 1674 # Python's garbage collector triggers a GC each time a certain number
1669 1675 # of container objects (the number being defined by
1670 1676 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1671 1677 # for each file in the dirstate. The C version then immediately marks
1672 1678 # them as not to be tracked by the collector. However, this has no
1673 1679 # effect on when GCs are triggered, only on what objects the GC looks
1674 1680 # into. This means that O(number of files) GCs are unavoidable.
1675 1681 # Depending on when in the process's lifetime the dirstate is parsed,
1676 1682 # this can get very expensive. As a workaround, disable GC while
1677 1683 # parsing the dirstate.
1678 1684 #
1679 1685 # (we cannot decorate the function directly since it is in a C module)
1680 1686 parse_dirstate = util.nogc(parsers.parse_dirstate)
1681 1687 p = parse_dirstate(self._map, self.copymap, st)
1682 1688 if not self._dirtyparents:
1683 1689 self.setparents(*p)
1684 1690
1685 1691 # Avoid excess attribute lookups by fast pathing certain checks
1686 1692 self.__contains__ = self._map.__contains__
1687 1693 self.__getitem__ = self._map.__getitem__
1688 1694 self.get = self._map.get
1689 1695
1690 1696 def write(self, st, now):
1691 1697 st.write(
1692 1698 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1693 1699 )
1694 1700 st.close()
1695 1701 self._dirtyparents = False
1696 1702 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1697 1703
1698 1704 @propertycache
1699 1705 def nonnormalset(self):
1700 1706 nonnorm, otherparents = self.nonnormalentries()
1701 1707 self.otherparentset = otherparents
1702 1708 return nonnorm
1703 1709
1704 1710 @propertycache
1705 1711 def otherparentset(self):
1706 1712 nonnorm, otherparents = self.nonnormalentries()
1707 1713 self.nonnormalset = nonnorm
1708 1714 return otherparents
1709 1715
1710 1716 @propertycache
1711 1717 def identity(self):
1712 1718 self._map
1713 1719 return self.identity
1714 1720
1715 1721 @propertycache
1716 1722 def dirfoldmap(self):
1717 1723 f = {}
1718 1724 normcase = util.normcase
1719 1725 for name in self._dirs:
1720 1726 f[normcase(name)] = name
1721 1727 return f
1722 1728
1723 1729
1724 1730 if rustmod is not None:
1725 1731
1726 1732 class dirstatemap(object):
1727 1733 def __init__(self, ui, opener, root):
1728 1734 self._ui = ui
1729 1735 self._opener = opener
1730 1736 self._root = root
1731 1737 self._filename = b'dirstate'
1732 1738 self._parents = None
1733 1739 self._dirtyparents = False
1734 1740
1735 1741 # for consistent view between _pl() and _read() invocations
1736 1742 self._pendingmode = None
1737 1743
1738 1744 def addfile(self, *args, **kwargs):
1739 1745 return self._rustmap.addfile(*args, **kwargs)
1740 1746
1741 1747 def removefile(self, *args, **kwargs):
1742 1748 return self._rustmap.removefile(*args, **kwargs)
1743 1749
1744 1750 def dropfile(self, *args, **kwargs):
1745 1751 return self._rustmap.dropfile(*args, **kwargs)
1746 1752
1747 1753 def clearambiguoustimes(self, *args, **kwargs):
1748 1754 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1749 1755
1750 1756 def nonnormalentries(self):
1751 1757 return self._rustmap.nonnormalentries()
1752 1758
1753 1759 def get(self, *args, **kwargs):
1754 1760 return self._rustmap.get(*args, **kwargs)
1755 1761
1756 1762 @propertycache
1757 1763 def _rustmap(self):
1758 1764 """
1759 1765 Fills the Dirstatemap when called.
1760 1766 Use `self._inner_rustmap` if reading the dirstate is not necessary.
1761 1767 """
1762 1768 self._rustmap = self._inner_rustmap
1763 1769 self.read()
1764 1770 return self._rustmap
1765 1771
1766 1772 @propertycache
1767 1773 def _inner_rustmap(self):
1768 1774 """
1769 1775 Does not fill the Dirstatemap when called. This allows for
1770 1776 optimizations where only setting/getting the parents is needed.
1771 1777 """
1772 1778 self._inner_rustmap = rustmod.DirstateMap(self._root)
1773 1779 return self._inner_rustmap
1774 1780
1775 1781 @property
1776 1782 def copymap(self):
1777 1783 return self._rustmap.copymap()
1778 1784
1779 1785 def preload(self):
1780 1786 self._rustmap
1781 1787
1782 1788 def clear(self):
1783 1789 self._rustmap.clear()
1784 1790 self._inner_rustmap.clear()
1785 1791 self.setparents(nullid, nullid)
1786 1792 util.clearcachedproperty(self, b"_dirs")
1787 1793 util.clearcachedproperty(self, b"_alldirs")
1788 1794 util.clearcachedproperty(self, b"dirfoldmap")
1789 1795
1790 1796 def items(self):
1791 1797 return self._rustmap.items()
1792 1798
1793 1799 def keys(self):
1794 1800 return iter(self._rustmap)
1795 1801
1796 1802 def __contains__(self, key):
1797 1803 return key in self._rustmap
1798 1804
1799 1805 def __getitem__(self, item):
1800 1806 return self._rustmap[item]
1801 1807
1802 1808 def __len__(self):
1803 1809 return len(self._rustmap)
1804 1810
1805 1811 def __iter__(self):
1806 1812 return iter(self._rustmap)
1807 1813
1808 1814 # forward for python2,3 compat
1809 1815 iteritems = items
1810 1816
1811 1817 def _opendirstatefile(self):
1812 1818 fp, mode = txnutil.trypending(
1813 1819 self._root, self._opener, self._filename
1814 1820 )
1815 1821 if self._pendingmode is not None and self._pendingmode != mode:
1816 1822 fp.close()
1817 1823 raise error.Abort(
1818 1824 _(b'working directory state may be changed parallelly')
1819 1825 )
1820 1826 self._pendingmode = mode
1821 1827 return fp
1822 1828
1823 1829 def setparents(self, p1, p2):
1824 1830 self._rustmap.setparents(p1, p2)
1825 1831 self._parents = (p1, p2)
1826 1832 self._dirtyparents = True
1827 1833
1828 1834 def parents(self):
1829 1835 if not self._parents:
1830 1836 try:
1831 1837 fp = self._opendirstatefile()
1832 st = fp.read(40)
1838 st = fp.read(2 * self._nodelen)
1833 1839 fp.close()
1834 1840 except IOError as err:
1835 1841 if err.errno != errno.ENOENT:
1836 1842 raise
1837 1843 # File doesn't exist, so the current state is empty
1838 1844 st = b''
1839 1845
1840 1846 try:
1841 1847 self._parents = self._inner_rustmap.parents(st)
1842 1848 except ValueError:
1843 1849 raise error.Abort(
1844 1850 _(b'working directory state appears damaged!')
1845 1851 )
1846 1852
1847 1853 return self._parents
1848 1854
1849 1855 def read(self):
1850 1856 # ignore HG_PENDING because identity is used only for writing
1851 1857 self.identity = util.filestat.frompath(
1852 1858 self._opener.join(self._filename)
1853 1859 )
1854 1860
1855 1861 try:
1856 1862 fp = self._opendirstatefile()
1857 1863 try:
1858 1864 st = fp.read()
1859 1865 finally:
1860 1866 fp.close()
1861 1867 except IOError as err:
1862 1868 if err.errno != errno.ENOENT:
1863 1869 raise
1864 1870 return
1865 1871 if not st:
1866 1872 return
1867 1873
1868 1874 parse_dirstate = util.nogc(self._rustmap.read)
1869 1875 parents = parse_dirstate(st)
1870 1876 if parents and not self._dirtyparents:
1871 1877 self.setparents(*parents)
1872 1878
1873 1879 self.__contains__ = self._rustmap.__contains__
1874 1880 self.__getitem__ = self._rustmap.__getitem__
1875 1881 self.get = self._rustmap.get
1876 1882
1877 1883 def write(self, st, now):
1878 1884 parents = self.parents()
1879 1885 st.write(self._rustmap.write(parents[0], parents[1], now))
1880 1886 st.close()
1881 1887 self._dirtyparents = False
1882 1888
1883 1889 @propertycache
1884 1890 def filefoldmap(self):
1885 1891 """Returns a dictionary mapping normalized case paths to their
1886 1892 non-normalized versions.
1887 1893 """
1888 1894 return self._rustmap.filefoldmapasdict()
1889 1895
1890 1896 def hastrackeddir(self, d):
1891 1897 self._dirs # Trigger Python's propertycache
1892 1898 return self._rustmap.hastrackeddir(d)
1893 1899
1894 1900 def hasdir(self, d):
1895 1901 self._dirs # Trigger Python's propertycache
1896 1902 return self._rustmap.hasdir(d)
1897 1903
1898 1904 @propertycache
1899 1905 def _dirs(self):
1900 1906 return self._rustmap.getdirs()
1901 1907
1902 1908 @propertycache
1903 1909 def _alldirs(self):
1904 1910 return self._rustmap.getalldirs()
1905 1911
1906 1912 @propertycache
1907 1913 def identity(self):
1908 1914 self._rustmap
1909 1915 return self.identity
1910 1916
1911 1917 @property
1912 1918 def nonnormalset(self):
1913 1919 nonnorm = self._rustmap.non_normal_entries()
1914 1920 return nonnorm
1915 1921
1916 1922 @propertycache
1917 1923 def otherparentset(self):
1918 1924 otherparents = self._rustmap.other_parent_entries()
1919 1925 return otherparents
1920 1926
1921 1927 @propertycache
1922 1928 def dirfoldmap(self):
1923 1929 f = {}
1924 1930 normcase = util.normcase
1925 1931 for name in self._dirs:
1926 1932 f[normcase(name)] = name
1927 1933 return f
General Comments 0
You need to be logged in to leave comments. Login now