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