##// END OF EJS Templates
rust-dirstatemap: don't read the dirstate when requesting parents...
Raphaël Gomès -
r45358:18e36ff8 stable
parent child Browse files
Show More
@@ -1,1901 +1,1915 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 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 1110 ) = rustmod.status(
1111 1111 self._map._rustmap,
1112 1112 matcher,
1113 1113 self._rootdir,
1114 1114 self._ignorefiles(),
1115 1115 self._checkexec,
1116 1116 self._lastnormaltime,
1117 1117 bool(list_clean),
1118 1118 bool(list_ignored),
1119 1119 bool(list_unknown),
1120 1120 )
1121 1121 if self._ui.warn:
1122 1122 for item in warnings:
1123 1123 if isinstance(item, tuple):
1124 1124 file_path, syntax = item
1125 1125 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1126 1126 file_path,
1127 1127 syntax,
1128 1128 )
1129 1129 self._ui.warn(msg)
1130 1130 else:
1131 1131 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1132 1132 self._ui.warn(
1133 1133 msg
1134 1134 % (
1135 1135 pathutil.canonpath(
1136 1136 self._rootdir, self._rootdir, item
1137 1137 ),
1138 1138 b"No such file or directory",
1139 1139 )
1140 1140 )
1141 1141
1142 1142 for (fn, message) in bad:
1143 1143 matcher.bad(fn, encoding.strtolocal(message))
1144 1144
1145 1145 status = scmutil.status(
1146 1146 modified=modified,
1147 1147 added=added,
1148 1148 removed=removed,
1149 1149 deleted=deleted,
1150 1150 unknown=unknown,
1151 1151 ignored=ignored,
1152 1152 clean=clean,
1153 1153 )
1154 1154 return (lookup, status)
1155 1155
1156 1156 def status(self, match, subrepos, ignored, clean, unknown):
1157 1157 '''Determine the status of the working copy relative to the
1158 1158 dirstate and return a pair of (unsure, status), where status is of type
1159 1159 scmutil.status and:
1160 1160
1161 1161 unsure:
1162 1162 files that might have been modified since the dirstate was
1163 1163 written, but need to be read to be sure (size is the same
1164 1164 but mtime differs)
1165 1165 status.modified:
1166 1166 files that have definitely been modified since the dirstate
1167 1167 was written (different size or mode)
1168 1168 status.clean:
1169 1169 files that have definitely not been modified since the
1170 1170 dirstate was written
1171 1171 '''
1172 1172 listignored, listclean, listunknown = ignored, clean, unknown
1173 1173 lookup, modified, added, unknown, ignored = [], [], [], [], []
1174 1174 removed, deleted, clean = [], [], []
1175 1175
1176 1176 dmap = self._map
1177 1177 dmap.preload()
1178 1178
1179 1179 use_rust = True
1180 1180
1181 1181 allowed_matchers = (
1182 1182 matchmod.alwaysmatcher,
1183 1183 matchmod.exactmatcher,
1184 1184 matchmod.includematcher,
1185 1185 )
1186 1186
1187 1187 if rustmod is None:
1188 1188 use_rust = False
1189 1189 elif self._checkcase:
1190 1190 # Case-insensitive filesystems are not handled yet
1191 1191 use_rust = False
1192 1192 elif subrepos:
1193 1193 use_rust = False
1194 1194 elif sparse.enabled:
1195 1195 use_rust = False
1196 1196 elif match.traversedir is not None:
1197 1197 use_rust = False
1198 1198 elif not isinstance(match, allowed_matchers):
1199 1199 # Matchers have yet to be implemented
1200 1200 use_rust = False
1201 1201
1202 1202 if use_rust:
1203 1203 try:
1204 1204 return self._rust_status(
1205 1205 match, listclean, listignored, listunknown
1206 1206 )
1207 1207 except rustmod.FallbackError:
1208 1208 pass
1209 1209
1210 1210 def noop(f):
1211 1211 pass
1212 1212
1213 1213 dcontains = dmap.__contains__
1214 1214 dget = dmap.__getitem__
1215 1215 ladd = lookup.append # aka "unsure"
1216 1216 madd = modified.append
1217 1217 aadd = added.append
1218 1218 uadd = unknown.append if listunknown else noop
1219 1219 iadd = ignored.append if listignored else noop
1220 1220 radd = removed.append
1221 1221 dadd = deleted.append
1222 1222 cadd = clean.append if listclean else noop
1223 1223 mexact = match.exact
1224 1224 dirignore = self._dirignore
1225 1225 checkexec = self._checkexec
1226 1226 copymap = self._map.copymap
1227 1227 lastnormaltime = self._lastnormaltime
1228 1228
1229 1229 # We need to do full walks when either
1230 1230 # - we're listing all clean files, or
1231 1231 # - match.traversedir does something, because match.traversedir should
1232 1232 # be called for every dir in the working dir
1233 1233 full = listclean or match.traversedir is not None
1234 1234 for fn, st in pycompat.iteritems(
1235 1235 self.walk(match, subrepos, listunknown, listignored, full=full)
1236 1236 ):
1237 1237 if not dcontains(fn):
1238 1238 if (listignored or mexact(fn)) and dirignore(fn):
1239 1239 if listignored:
1240 1240 iadd(fn)
1241 1241 else:
1242 1242 uadd(fn)
1243 1243 continue
1244 1244
1245 1245 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1246 1246 # written like that for performance reasons. dmap[fn] is not a
1247 1247 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1248 1248 # opcode has fast paths when the value to be unpacked is a tuple or
1249 1249 # a list, but falls back to creating a full-fledged iterator in
1250 1250 # general. That is much slower than simply accessing and storing the
1251 1251 # tuple members one by one.
1252 1252 t = dget(fn)
1253 1253 state = t[0]
1254 1254 mode = t[1]
1255 1255 size = t[2]
1256 1256 time = t[3]
1257 1257
1258 1258 if not st and state in b"nma":
1259 1259 dadd(fn)
1260 1260 elif state == b'n':
1261 1261 if (
1262 1262 size >= 0
1263 1263 and (
1264 1264 (size != st.st_size and size != st.st_size & _rangemask)
1265 1265 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1266 1266 )
1267 1267 or size == -2 # other parent
1268 1268 or fn in copymap
1269 1269 ):
1270 1270 madd(fn)
1271 1271 elif (
1272 1272 time != st[stat.ST_MTIME]
1273 1273 and time != st[stat.ST_MTIME] & _rangemask
1274 1274 ):
1275 1275 ladd(fn)
1276 1276 elif st[stat.ST_MTIME] == lastnormaltime:
1277 1277 # fn may have just been marked as normal and it may have
1278 1278 # changed in the same second without changing its size.
1279 1279 # This can happen if we quickly do multiple commits.
1280 1280 # Force lookup, so we don't miss such a racy file change.
1281 1281 ladd(fn)
1282 1282 elif listclean:
1283 1283 cadd(fn)
1284 1284 elif state == b'm':
1285 1285 madd(fn)
1286 1286 elif state == b'a':
1287 1287 aadd(fn)
1288 1288 elif state == b'r':
1289 1289 radd(fn)
1290 1290 status = scmutil.status(
1291 1291 modified, added, removed, deleted, unknown, ignored, clean
1292 1292 )
1293 1293 return (lookup, status)
1294 1294
1295 1295 def matches(self, match):
1296 1296 '''
1297 1297 return files in the dirstate (in whatever state) filtered by match
1298 1298 '''
1299 1299 dmap = self._map
1300 1300 if rustmod is not None:
1301 1301 dmap = self._map._rustmap
1302 1302
1303 1303 if match.always():
1304 1304 return dmap.keys()
1305 1305 files = match.files()
1306 1306 if match.isexact():
1307 1307 # fast path -- filter the other way around, since typically files is
1308 1308 # much smaller than dmap
1309 1309 return [f for f in files if f in dmap]
1310 1310 if match.prefix() and all(fn in dmap for fn in files):
1311 1311 # fast path -- all the values are known to be files, so just return
1312 1312 # that
1313 1313 return list(files)
1314 1314 return [f for f in dmap if match(f)]
1315 1315
1316 1316 def _actualfilename(self, tr):
1317 1317 if tr:
1318 1318 return self._pendingfilename
1319 1319 else:
1320 1320 return self._filename
1321 1321
1322 1322 def savebackup(self, tr, backupname):
1323 1323 '''Save current dirstate into backup file'''
1324 1324 filename = self._actualfilename(tr)
1325 1325 assert backupname != filename
1326 1326
1327 1327 # use '_writedirstate' instead of 'write' to write changes certainly,
1328 1328 # because the latter omits writing out if transaction is running.
1329 1329 # output file will be used to create backup of dirstate at this point.
1330 1330 if self._dirty or not self._opener.exists(filename):
1331 1331 self._writedirstate(
1332 1332 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1333 1333 )
1334 1334
1335 1335 if tr:
1336 1336 # ensure that subsequent tr.writepending returns True for
1337 1337 # changes written out above, even if dirstate is never
1338 1338 # changed after this
1339 1339 tr.addfilegenerator(
1340 1340 b'dirstate',
1341 1341 (self._filename,),
1342 1342 self._writedirstate,
1343 1343 location=b'plain',
1344 1344 )
1345 1345
1346 1346 # ensure that pending file written above is unlinked at
1347 1347 # failure, even if tr.writepending isn't invoked until the
1348 1348 # end of this transaction
1349 1349 tr.registertmp(filename, location=b'plain')
1350 1350
1351 1351 self._opener.tryunlink(backupname)
1352 1352 # hardlink backup is okay because _writedirstate is always called
1353 1353 # with an "atomictemp=True" file.
1354 1354 util.copyfile(
1355 1355 self._opener.join(filename),
1356 1356 self._opener.join(backupname),
1357 1357 hardlink=True,
1358 1358 )
1359 1359
1360 1360 def restorebackup(self, tr, backupname):
1361 1361 '''Restore dirstate by backup file'''
1362 1362 # this "invalidate()" prevents "wlock.release()" from writing
1363 1363 # changes of dirstate out after restoring from backup file
1364 1364 self.invalidate()
1365 1365 filename = self._actualfilename(tr)
1366 1366 o = self._opener
1367 1367 if util.samefile(o.join(backupname), o.join(filename)):
1368 1368 o.unlink(backupname)
1369 1369 else:
1370 1370 o.rename(backupname, filename, checkambig=True)
1371 1371
1372 1372 def clearbackup(self, tr, backupname):
1373 1373 '''Clear backup file'''
1374 1374 self._opener.unlink(backupname)
1375 1375
1376 1376
1377 1377 class dirstatemap(object):
1378 1378 """Map encapsulating the dirstate's contents.
1379 1379
1380 1380 The dirstate contains the following state:
1381 1381
1382 1382 - `identity` is the identity of the dirstate file, which can be used to
1383 1383 detect when changes have occurred to the dirstate file.
1384 1384
1385 1385 - `parents` is a pair containing the parents of the working copy. The
1386 1386 parents are updated by calling `setparents`.
1387 1387
1388 1388 - the state map maps filenames to tuples of (state, mode, size, mtime),
1389 1389 where state is a single character representing 'normal', 'added',
1390 1390 'removed', or 'merged'. It is read by treating the dirstate as a
1391 1391 dict. File state is updated by calling the `addfile`, `removefile` and
1392 1392 `dropfile` methods.
1393 1393
1394 1394 - `copymap` maps destination filenames to their source filename.
1395 1395
1396 1396 The dirstate also provides the following views onto the state:
1397 1397
1398 1398 - `nonnormalset` is a set of the filenames that have state other
1399 1399 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1400 1400
1401 1401 - `otherparentset` is a set of the filenames that are marked as coming
1402 1402 from the second parent when the dirstate is currently being merged.
1403 1403
1404 1404 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1405 1405 form that they appear as in the dirstate.
1406 1406
1407 1407 - `dirfoldmap` is a dict mapping normalized directory names to the
1408 1408 denormalized form that they appear as in the dirstate.
1409 1409 """
1410 1410
1411 1411 def __init__(self, ui, opener, root):
1412 1412 self._ui = ui
1413 1413 self._opener = opener
1414 1414 self._root = root
1415 1415 self._filename = b'dirstate'
1416 1416
1417 1417 self._parents = None
1418 1418 self._dirtyparents = False
1419 1419
1420 1420 # for consistent view between _pl() and _read() invocations
1421 1421 self._pendingmode = None
1422 1422
1423 1423 @propertycache
1424 1424 def _map(self):
1425 1425 self._map = {}
1426 1426 self.read()
1427 1427 return self._map
1428 1428
1429 1429 @propertycache
1430 1430 def copymap(self):
1431 1431 self.copymap = {}
1432 1432 self._map
1433 1433 return self.copymap
1434 1434
1435 1435 def clear(self):
1436 1436 self._map.clear()
1437 1437 self.copymap.clear()
1438 1438 self.setparents(nullid, nullid)
1439 1439 util.clearcachedproperty(self, b"_dirs")
1440 1440 util.clearcachedproperty(self, b"_alldirs")
1441 1441 util.clearcachedproperty(self, b"filefoldmap")
1442 1442 util.clearcachedproperty(self, b"dirfoldmap")
1443 1443 util.clearcachedproperty(self, b"nonnormalset")
1444 1444 util.clearcachedproperty(self, b"otherparentset")
1445 1445
1446 1446 def items(self):
1447 1447 return pycompat.iteritems(self._map)
1448 1448
1449 1449 # forward for python2,3 compat
1450 1450 iteritems = items
1451 1451
1452 1452 def __len__(self):
1453 1453 return len(self._map)
1454 1454
1455 1455 def __iter__(self):
1456 1456 return iter(self._map)
1457 1457
1458 1458 def get(self, key, default=None):
1459 1459 return self._map.get(key, default)
1460 1460
1461 1461 def __contains__(self, key):
1462 1462 return key in self._map
1463 1463
1464 1464 def __getitem__(self, key):
1465 1465 return self._map[key]
1466 1466
1467 1467 def keys(self):
1468 1468 return self._map.keys()
1469 1469
1470 1470 def preload(self):
1471 1471 """Loads the underlying data, if it's not already loaded"""
1472 1472 self._map
1473 1473
1474 1474 def addfile(self, f, oldstate, state, mode, size, mtime):
1475 1475 """Add a tracked file to the dirstate."""
1476 1476 if oldstate in b"?r" and "_dirs" in self.__dict__:
1477 1477 self._dirs.addpath(f)
1478 1478 if oldstate == b"?" and "_alldirs" in self.__dict__:
1479 1479 self._alldirs.addpath(f)
1480 1480 self._map[f] = dirstatetuple(state, mode, size, mtime)
1481 1481 if state != b'n' or mtime == -1:
1482 1482 self.nonnormalset.add(f)
1483 1483 if size == -2:
1484 1484 self.otherparentset.add(f)
1485 1485
1486 1486 def removefile(self, f, oldstate, size):
1487 1487 """
1488 1488 Mark a file as removed in the dirstate.
1489 1489
1490 1490 The `size` parameter is used to store sentinel values that indicate
1491 1491 the file's previous state. In the future, we should refactor this
1492 1492 to be more explicit about what that state is.
1493 1493 """
1494 1494 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1495 1495 self._dirs.delpath(f)
1496 1496 if oldstate == b"?" and "_alldirs" in self.__dict__:
1497 1497 self._alldirs.addpath(f)
1498 1498 if "filefoldmap" in self.__dict__:
1499 1499 normed = util.normcase(f)
1500 1500 self.filefoldmap.pop(normed, None)
1501 1501 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1502 1502 self.nonnormalset.add(f)
1503 1503
1504 1504 def dropfile(self, f, oldstate):
1505 1505 """
1506 1506 Remove a file from the dirstate. Returns True if the file was
1507 1507 previously recorded.
1508 1508 """
1509 1509 exists = self._map.pop(f, None) is not None
1510 1510 if exists:
1511 1511 if oldstate != b"r" and "_dirs" in self.__dict__:
1512 1512 self._dirs.delpath(f)
1513 1513 if "_alldirs" in self.__dict__:
1514 1514 self._alldirs.delpath(f)
1515 1515 if "filefoldmap" in self.__dict__:
1516 1516 normed = util.normcase(f)
1517 1517 self.filefoldmap.pop(normed, None)
1518 1518 self.nonnormalset.discard(f)
1519 1519 return exists
1520 1520
1521 1521 def clearambiguoustimes(self, files, now):
1522 1522 for f in files:
1523 1523 e = self.get(f)
1524 1524 if e is not None and e[0] == b'n' and e[3] == now:
1525 1525 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1526 1526 self.nonnormalset.add(f)
1527 1527
1528 1528 def nonnormalentries(self):
1529 1529 '''Compute the nonnormal dirstate entries from the dmap'''
1530 1530 try:
1531 1531 return parsers.nonnormalotherparententries(self._map)
1532 1532 except AttributeError:
1533 1533 nonnorm = set()
1534 1534 otherparent = set()
1535 1535 for fname, e in pycompat.iteritems(self._map):
1536 1536 if e[0] != b'n' or e[3] == -1:
1537 1537 nonnorm.add(fname)
1538 1538 if e[0] == b'n' and e[2] == -2:
1539 1539 otherparent.add(fname)
1540 1540 return nonnorm, otherparent
1541 1541
1542 1542 @propertycache
1543 1543 def filefoldmap(self):
1544 1544 """Returns a dictionary mapping normalized case paths to their
1545 1545 non-normalized versions.
1546 1546 """
1547 1547 try:
1548 1548 makefilefoldmap = parsers.make_file_foldmap
1549 1549 except AttributeError:
1550 1550 pass
1551 1551 else:
1552 1552 return makefilefoldmap(
1553 1553 self._map, util.normcasespec, util.normcasefallback
1554 1554 )
1555 1555
1556 1556 f = {}
1557 1557 normcase = util.normcase
1558 1558 for name, s in pycompat.iteritems(self._map):
1559 1559 if s[0] != b'r':
1560 1560 f[normcase(name)] = name
1561 1561 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1562 1562 return f
1563 1563
1564 1564 def hastrackeddir(self, d):
1565 1565 """
1566 1566 Returns True if the dirstate contains a tracked (not removed) file
1567 1567 in this directory.
1568 1568 """
1569 1569 return d in self._dirs
1570 1570
1571 1571 def hasdir(self, d):
1572 1572 """
1573 1573 Returns True if the dirstate contains a file (tracked or removed)
1574 1574 in this directory.
1575 1575 """
1576 1576 return d in self._alldirs
1577 1577
1578 1578 @propertycache
1579 1579 def _dirs(self):
1580 1580 return pathutil.dirs(self._map, b'r')
1581 1581
1582 1582 @propertycache
1583 1583 def _alldirs(self):
1584 1584 return pathutil.dirs(self._map)
1585 1585
1586 1586 def _opendirstatefile(self):
1587 1587 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1588 1588 if self._pendingmode is not None and self._pendingmode != mode:
1589 1589 fp.close()
1590 1590 raise error.Abort(
1591 1591 _(b'working directory state may be changed parallelly')
1592 1592 )
1593 1593 self._pendingmode = mode
1594 1594 return fp
1595 1595
1596 1596 def parents(self):
1597 1597 if not self._parents:
1598 1598 try:
1599 1599 fp = self._opendirstatefile()
1600 1600 st = fp.read(40)
1601 1601 fp.close()
1602 1602 except IOError as err:
1603 1603 if err.errno != errno.ENOENT:
1604 1604 raise
1605 1605 # File doesn't exist, so the current state is empty
1606 1606 st = b''
1607 1607
1608 1608 l = len(st)
1609 1609 if l == 40:
1610 1610 self._parents = (st[:20], st[20:40])
1611 1611 elif l == 0:
1612 1612 self._parents = (nullid, nullid)
1613 1613 else:
1614 1614 raise error.Abort(
1615 1615 _(b'working directory state appears damaged!')
1616 1616 )
1617 1617
1618 1618 return self._parents
1619 1619
1620 1620 def setparents(self, p1, p2):
1621 1621 self._parents = (p1, p2)
1622 1622 self._dirtyparents = True
1623 1623
1624 1624 def read(self):
1625 1625 # ignore HG_PENDING because identity is used only for writing
1626 1626 self.identity = util.filestat.frompath(
1627 1627 self._opener.join(self._filename)
1628 1628 )
1629 1629
1630 1630 try:
1631 1631 fp = self._opendirstatefile()
1632 1632 try:
1633 1633 st = fp.read()
1634 1634 finally:
1635 1635 fp.close()
1636 1636 except IOError as err:
1637 1637 if err.errno != errno.ENOENT:
1638 1638 raise
1639 1639 return
1640 1640 if not st:
1641 1641 return
1642 1642
1643 1643 if util.safehasattr(parsers, b'dict_new_presized'):
1644 1644 # Make an estimate of the number of files in the dirstate based on
1645 1645 # its size. From a linear regression on a set of real-world repos,
1646 1646 # all over 10,000 files, the size of a dirstate entry is 85
1647 1647 # bytes. The cost of resizing is significantly higher than the cost
1648 1648 # of filling in a larger presized dict, so subtract 20% from the
1649 1649 # size.
1650 1650 #
1651 1651 # This heuristic is imperfect in many ways, so in a future dirstate
1652 1652 # format update it makes sense to just record the number of entries
1653 1653 # on write.
1654 1654 self._map = parsers.dict_new_presized(len(st) // 71)
1655 1655
1656 1656 # Python's garbage collector triggers a GC each time a certain number
1657 1657 # of container objects (the number being defined by
1658 1658 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1659 1659 # for each file in the dirstate. The C version then immediately marks
1660 1660 # them as not to be tracked by the collector. However, this has no
1661 1661 # effect on when GCs are triggered, only on what objects the GC looks
1662 1662 # into. This means that O(number of files) GCs are unavoidable.
1663 1663 # Depending on when in the process's lifetime the dirstate is parsed,
1664 1664 # this can get very expensive. As a workaround, disable GC while
1665 1665 # parsing the dirstate.
1666 1666 #
1667 1667 # (we cannot decorate the function directly since it is in a C module)
1668 1668 parse_dirstate = util.nogc(parsers.parse_dirstate)
1669 1669 p = parse_dirstate(self._map, self.copymap, st)
1670 1670 if not self._dirtyparents:
1671 1671 self.setparents(*p)
1672 1672
1673 1673 # Avoid excess attribute lookups by fast pathing certain checks
1674 1674 self.__contains__ = self._map.__contains__
1675 1675 self.__getitem__ = self._map.__getitem__
1676 1676 self.get = self._map.get
1677 1677
1678 1678 def write(self, st, now):
1679 1679 st.write(
1680 1680 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1681 1681 )
1682 1682 st.close()
1683 1683 self._dirtyparents = False
1684 1684 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1685 1685
1686 1686 @propertycache
1687 1687 def nonnormalset(self):
1688 1688 nonnorm, otherparents = self.nonnormalentries()
1689 1689 self.otherparentset = otherparents
1690 1690 return nonnorm
1691 1691
1692 1692 @propertycache
1693 1693 def otherparentset(self):
1694 1694 nonnorm, otherparents = self.nonnormalentries()
1695 1695 self.nonnormalset = nonnorm
1696 1696 return otherparents
1697 1697
1698 1698 @propertycache
1699 1699 def identity(self):
1700 1700 self._map
1701 1701 return self.identity
1702 1702
1703 1703 @propertycache
1704 1704 def dirfoldmap(self):
1705 1705 f = {}
1706 1706 normcase = util.normcase
1707 1707 for name in self._dirs:
1708 1708 f[normcase(name)] = name
1709 1709 return f
1710 1710
1711 1711
1712 1712 if rustmod is not None:
1713 1713
1714 1714 class dirstatemap(object):
1715 1715 def __init__(self, ui, opener, root):
1716 1716 self._ui = ui
1717 1717 self._opener = opener
1718 1718 self._root = root
1719 1719 self._filename = b'dirstate'
1720 1720 self._parents = None
1721 1721 self._dirtyparents = False
1722 1722
1723 1723 # for consistent view between _pl() and _read() invocations
1724 1724 self._pendingmode = None
1725 1725
1726 1726 def addfile(self, *args, **kwargs):
1727 1727 return self._rustmap.addfile(*args, **kwargs)
1728 1728
1729 1729 def removefile(self, *args, **kwargs):
1730 1730 return self._rustmap.removefile(*args, **kwargs)
1731 1731
1732 1732 def dropfile(self, *args, **kwargs):
1733 1733 return self._rustmap.dropfile(*args, **kwargs)
1734 1734
1735 1735 def clearambiguoustimes(self, *args, **kwargs):
1736 1736 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1737 1737
1738 1738 def nonnormalentries(self):
1739 1739 return self._rustmap.nonnormalentries()
1740 1740
1741 1741 def get(self, *args, **kwargs):
1742 1742 return self._rustmap.get(*args, **kwargs)
1743 1743
1744 1744 @propertycache
1745 1745 def _rustmap(self):
1746 self._rustmap = rustmod.DirstateMap(self._root)
1746 """
1747 Fills the Dirstatemap when called.
1748 Use `self._inner_rustmap` if reading the dirstate is not necessary.
1749 """
1750 self._rustmap = self._inner_rustmap
1747 1751 self.read()
1748 1752 return self._rustmap
1749 1753
1754 @propertycache
1755 def _inner_rustmap(self):
1756 """
1757 Does not fill the Dirstatemap when called. This allows for
1758 optimizations where only setting/getting the parents is needed.
1759 """
1760 self._inner_rustmap = rustmod.DirstateMap(self._root)
1761 return self._inner_rustmap
1762
1750 1763 @property
1751 1764 def copymap(self):
1752 1765 return self._rustmap.copymap()
1753 1766
1754 1767 def preload(self):
1755 1768 self._rustmap
1756 1769
1757 1770 def clear(self):
1758 1771 self._rustmap.clear()
1772 self._inner_rustmap.clear()
1759 1773 self.setparents(nullid, nullid)
1760 1774 util.clearcachedproperty(self, b"_dirs")
1761 1775 util.clearcachedproperty(self, b"_alldirs")
1762 1776 util.clearcachedproperty(self, b"dirfoldmap")
1763 1777
1764 1778 def items(self):
1765 1779 return self._rustmap.items()
1766 1780
1767 1781 def keys(self):
1768 1782 return iter(self._rustmap)
1769 1783
1770 1784 def __contains__(self, key):
1771 1785 return key in self._rustmap
1772 1786
1773 1787 def __getitem__(self, item):
1774 1788 return self._rustmap[item]
1775 1789
1776 1790 def __len__(self):
1777 1791 return len(self._rustmap)
1778 1792
1779 1793 def __iter__(self):
1780 1794 return iter(self._rustmap)
1781 1795
1782 1796 # forward for python2,3 compat
1783 1797 iteritems = items
1784 1798
1785 1799 def _opendirstatefile(self):
1786 1800 fp, mode = txnutil.trypending(
1787 1801 self._root, self._opener, self._filename
1788 1802 )
1789 1803 if self._pendingmode is not None and self._pendingmode != mode:
1790 1804 fp.close()
1791 1805 raise error.Abort(
1792 1806 _(b'working directory state may be changed parallelly')
1793 1807 )
1794 1808 self._pendingmode = mode
1795 1809 return fp
1796 1810
1797 1811 def setparents(self, p1, p2):
1798 1812 self._rustmap.setparents(p1, p2)
1799 1813 self._parents = (p1, p2)
1800 1814 self._dirtyparents = True
1801 1815
1802 1816 def parents(self):
1803 1817 if not self._parents:
1804 1818 try:
1805 1819 fp = self._opendirstatefile()
1806 1820 st = fp.read(40)
1807 1821 fp.close()
1808 1822 except IOError as err:
1809 1823 if err.errno != errno.ENOENT:
1810 1824 raise
1811 1825 # File doesn't exist, so the current state is empty
1812 1826 st = b''
1813 1827
1814 1828 try:
1815 self._parents = self._rustmap.parents(st)
1829 self._parents = self._inner_rustmap.parents(st)
1816 1830 except ValueError:
1817 1831 raise error.Abort(
1818 1832 _(b'working directory state appears damaged!')
1819 1833 )
1820 1834
1821 1835 return self._parents
1822 1836
1823 1837 def read(self):
1824 1838 # ignore HG_PENDING because identity is used only for writing
1825 1839 self.identity = util.filestat.frompath(
1826 1840 self._opener.join(self._filename)
1827 1841 )
1828 1842
1829 1843 try:
1830 1844 fp = self._opendirstatefile()
1831 1845 try:
1832 1846 st = fp.read()
1833 1847 finally:
1834 1848 fp.close()
1835 1849 except IOError as err:
1836 1850 if err.errno != errno.ENOENT:
1837 1851 raise
1838 1852 return
1839 1853 if not st:
1840 1854 return
1841 1855
1842 1856 parse_dirstate = util.nogc(self._rustmap.read)
1843 1857 parents = parse_dirstate(st)
1844 1858 if parents and not self._dirtyparents:
1845 1859 self.setparents(*parents)
1846 1860
1847 1861 self.__contains__ = self._rustmap.__contains__
1848 1862 self.__getitem__ = self._rustmap.__getitem__
1849 1863 self.get = self._rustmap.get
1850 1864
1851 1865 def write(self, st, now):
1852 1866 parents = self.parents()
1853 1867 st.write(self._rustmap.write(parents[0], parents[1], now))
1854 1868 st.close()
1855 1869 self._dirtyparents = False
1856 1870
1857 1871 @propertycache
1858 1872 def filefoldmap(self):
1859 1873 """Returns a dictionary mapping normalized case paths to their
1860 1874 non-normalized versions.
1861 1875 """
1862 1876 return self._rustmap.filefoldmapasdict()
1863 1877
1864 1878 def hastrackeddir(self, d):
1865 1879 self._dirs # Trigger Python's propertycache
1866 1880 return self._rustmap.hastrackeddir(d)
1867 1881
1868 1882 def hasdir(self, d):
1869 1883 self._dirs # Trigger Python's propertycache
1870 1884 return self._rustmap.hasdir(d)
1871 1885
1872 1886 @propertycache
1873 1887 def _dirs(self):
1874 1888 return self._rustmap.getdirs()
1875 1889
1876 1890 @propertycache
1877 1891 def _alldirs(self):
1878 1892 return self._rustmap.getalldirs()
1879 1893
1880 1894 @propertycache
1881 1895 def identity(self):
1882 1896 self._rustmap
1883 1897 return self.identity
1884 1898
1885 1899 @property
1886 1900 def nonnormalset(self):
1887 1901 nonnorm = self._rustmap.non_normal_entries()
1888 1902 return nonnorm
1889 1903
1890 1904 @propertycache
1891 1905 def otherparentset(self):
1892 1906 otherparents = self._rustmap.other_parent_entries()
1893 1907 return otherparents
1894 1908
1895 1909 @propertycache
1896 1910 def dirfoldmap(self):
1897 1911 f = {}
1898 1912 normcase = util.normcase
1899 1913 for name in self._dirs:
1900 1914 f[normcase(name)] = name
1901 1915 return f
General Comments 0
You need to be logged in to leave comments. Login now