##// END OF EJS Templates
dirstate: deprecate `__getitem__` access...
marmoute -
r48919:dcd97b08 default
parent child Browse files
Show More
@@ -1,1546 +1,1548 b''
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@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 .pycompat import delattr
18 18
19 19 from hgdemandimport import tracing
20 20
21 21 from . import (
22 22 dirstatemap,
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 util,
32 32 )
33 33
34 34 from .interfaces import (
35 35 dirstate as intdirstate,
36 36 util as interfaceutil,
37 37 )
38 38
39 39 parsers = policy.importmod('parsers')
40 40 rustmod = policy.importrust('dirstate')
41 41
42 42 SUPPORTS_DIRSTATE_V2 = rustmod is not None
43 43
44 44 propertycache = util.propertycache
45 45 filecache = scmutil.filecache
46 46 _rangemask = dirstatemap.rangemask
47 47
48 48 DirstateItem = dirstatemap.DirstateItem
49 49
50 50
51 51 class repocache(filecache):
52 52 """filecache for files in .hg/"""
53 53
54 54 def join(self, obj, fname):
55 55 return obj._opener.join(fname)
56 56
57 57
58 58 class rootcache(filecache):
59 59 """filecache for files in the repository root"""
60 60
61 61 def join(self, obj, fname):
62 62 return obj._join(fname)
63 63
64 64
65 65 def _getfsnow(vfs):
66 66 '''Get "now" timestamp on filesystem'''
67 67 tmpfd, tmpname = vfs.mkstemp()
68 68 try:
69 69 return os.fstat(tmpfd)[stat.ST_MTIME]
70 70 finally:
71 71 os.close(tmpfd)
72 72 vfs.unlink(tmpname)
73 73
74 74
75 75 def requires_parents_change(func):
76 76 def wrap(self, *args, **kwargs):
77 77 if not self.pendingparentchange():
78 78 msg = 'calling `%s` outside of a parentchange context'
79 79 msg %= func.__name__
80 80 raise error.ProgrammingError(msg)
81 81 return func(self, *args, **kwargs)
82 82
83 83 return wrap
84 84
85 85
86 86 def requires_no_parents_change(func):
87 87 def wrap(self, *args, **kwargs):
88 88 if self.pendingparentchange():
89 89 msg = 'calling `%s` inside of a parentchange context'
90 90 msg %= func.__name__
91 91 raise error.ProgrammingError(msg)
92 92 return func(self, *args, **kwargs)
93 93
94 94 return wrap
95 95
96 96
97 97 @interfaceutil.implementer(intdirstate.idirstate)
98 98 class dirstate(object):
99 99 def __init__(
100 100 self,
101 101 opener,
102 102 ui,
103 103 root,
104 104 validate,
105 105 sparsematchfn,
106 106 nodeconstants,
107 107 use_dirstate_v2,
108 108 ):
109 109 """Create a new dirstate object.
110 110
111 111 opener is an open()-like callable that can be used to open the
112 112 dirstate file; root is the root of the directory tracked by
113 113 the dirstate.
114 114 """
115 115 self._use_dirstate_v2 = use_dirstate_v2
116 116 self._nodeconstants = nodeconstants
117 117 self._opener = opener
118 118 self._validate = validate
119 119 self._root = root
120 120 self._sparsematchfn = sparsematchfn
121 121 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
122 122 # UNC path pointing to root share (issue4557)
123 123 self._rootdir = pathutil.normasprefix(root)
124 124 self._dirty = False
125 125 self._lastnormaltime = 0
126 126 self._ui = ui
127 127 self._filecache = {}
128 128 self._parentwriters = 0
129 129 self._filename = b'dirstate'
130 130 self._pendingfilename = b'%s.pending' % self._filename
131 131 self._plchangecallbacks = {}
132 132 self._origpl = None
133 133 self._mapcls = dirstatemap.dirstatemap
134 134 # Access and cache cwd early, so we don't access it for the first time
135 135 # after a working-copy update caused it to not exist (accessing it then
136 136 # raises an exception).
137 137 self._cwd
138 138
139 139 def prefetch_parents(self):
140 140 """make sure the parents are loaded
141 141
142 142 Used to avoid a race condition.
143 143 """
144 144 self._pl
145 145
146 146 @contextlib.contextmanager
147 147 def parentchange(self):
148 148 """Context manager for handling dirstate parents.
149 149
150 150 If an exception occurs in the scope of the context manager,
151 151 the incoherent dirstate won't be written when wlock is
152 152 released.
153 153 """
154 154 self._parentwriters += 1
155 155 yield
156 156 # Typically we want the "undo" step of a context manager in a
157 157 # finally block so it happens even when an exception
158 158 # occurs. In this case, however, we only want to decrement
159 159 # parentwriters if the code in the with statement exits
160 160 # normally, so we don't have a try/finally here on purpose.
161 161 self._parentwriters -= 1
162 162
163 163 def pendingparentchange(self):
164 164 """Returns true if the dirstate is in the middle of a set of changes
165 165 that modify the dirstate parent.
166 166 """
167 167 return self._parentwriters > 0
168 168
169 169 @propertycache
170 170 def _map(self):
171 171 """Return the dirstate contents (see documentation for dirstatemap)."""
172 172 self._map = self._mapcls(
173 173 self._ui,
174 174 self._opener,
175 175 self._root,
176 176 self._nodeconstants,
177 177 self._use_dirstate_v2,
178 178 )
179 179 return self._map
180 180
181 181 @property
182 182 def _sparsematcher(self):
183 183 """The matcher for the sparse checkout.
184 184
185 185 The working directory may not include every file from a manifest. The
186 186 matcher obtained by this property will match a path if it is to be
187 187 included in the working directory.
188 188 """
189 189 # TODO there is potential to cache this property. For now, the matcher
190 190 # is resolved on every access. (But the called function does use a
191 191 # cache to keep the lookup fast.)
192 192 return self._sparsematchfn()
193 193
194 194 @repocache(b'branch')
195 195 def _branch(self):
196 196 try:
197 197 return self._opener.read(b"branch").strip() or b"default"
198 198 except IOError as inst:
199 199 if inst.errno != errno.ENOENT:
200 200 raise
201 201 return b"default"
202 202
203 203 @property
204 204 def _pl(self):
205 205 return self._map.parents()
206 206
207 207 def hasdir(self, d):
208 208 return self._map.hastrackeddir(d)
209 209
210 210 @rootcache(b'.hgignore')
211 211 def _ignore(self):
212 212 files = self._ignorefiles()
213 213 if not files:
214 214 return matchmod.never()
215 215
216 216 pats = [b'include:%s' % f for f in files]
217 217 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
218 218
219 219 @propertycache
220 220 def _slash(self):
221 221 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
222 222
223 223 @propertycache
224 224 def _checklink(self):
225 225 return util.checklink(self._root)
226 226
227 227 @propertycache
228 228 def _checkexec(self):
229 229 return bool(util.checkexec(self._root))
230 230
231 231 @propertycache
232 232 def _checkcase(self):
233 233 return not util.fscasesensitive(self._join(b'.hg'))
234 234
235 235 def _join(self, f):
236 236 # much faster than os.path.join()
237 237 # it's safe because f is always a relative path
238 238 return self._rootdir + f
239 239
240 240 def flagfunc(self, buildfallback):
241 241 if self._checklink and self._checkexec:
242 242
243 243 def f(x):
244 244 try:
245 245 st = os.lstat(self._join(x))
246 246 if util.statislink(st):
247 247 return b'l'
248 248 if util.statisexec(st):
249 249 return b'x'
250 250 except OSError:
251 251 pass
252 252 return b''
253 253
254 254 return f
255 255
256 256 fallback = buildfallback()
257 257 if self._checklink:
258 258
259 259 def f(x):
260 260 if os.path.islink(self._join(x)):
261 261 return b'l'
262 262 if b'x' in fallback(x):
263 263 return b'x'
264 264 return b''
265 265
266 266 return f
267 267 if self._checkexec:
268 268
269 269 def f(x):
270 270 if b'l' in fallback(x):
271 271 return b'l'
272 272 if util.isexec(self._join(x)):
273 273 return b'x'
274 274 return b''
275 275
276 276 return f
277 277 else:
278 278 return fallback
279 279
280 280 @propertycache
281 281 def _cwd(self):
282 282 # internal config: ui.forcecwd
283 283 forcecwd = self._ui.config(b'ui', b'forcecwd')
284 284 if forcecwd:
285 285 return forcecwd
286 286 return encoding.getcwd()
287 287
288 288 def getcwd(self):
289 289 """Return the path from which a canonical path is calculated.
290 290
291 291 This path should be used to resolve file patterns or to convert
292 292 canonical paths back to file paths for display. It shouldn't be
293 293 used to get real file paths. Use vfs functions instead.
294 294 """
295 295 cwd = self._cwd
296 296 if cwd == self._root:
297 297 return b''
298 298 # self._root ends with a path separator if self._root is '/' or 'C:\'
299 299 rootsep = self._root
300 300 if not util.endswithsep(rootsep):
301 301 rootsep += pycompat.ossep
302 302 if cwd.startswith(rootsep):
303 303 return cwd[len(rootsep) :]
304 304 else:
305 305 # we're outside the repo. return an absolute path.
306 306 return cwd
307 307
308 308 def pathto(self, f, cwd=None):
309 309 if cwd is None:
310 310 cwd = self.getcwd()
311 311 path = util.pathto(self._root, cwd, f)
312 312 if self._slash:
313 313 return util.pconvert(path)
314 314 return path
315 315
316 316 def __getitem__(self, key):
317 317 """Return the current state of key (a filename) in the dirstate.
318 318
319 319 States are:
320 320 n normal
321 321 m needs merging
322 322 r marked for removal
323 323 a marked for addition
324 324 ? not tracked
325 325
326 326 XXX The "state" is a bit obscure to be in the "public" API. we should
327 327 consider migrating all user of this to going through the dirstate entry
328 328 instead.
329 329 """
330 msg = b"don't use dirstate[file], use dirstate.get_entry(file)"
331 util.nouideprecwarn(msg, b'6.1', stacklevel=2)
330 332 entry = self._map.get(key)
331 333 if entry is not None:
332 334 return entry.state
333 335 return b'?'
334 336
335 337 def get_entry(self, path):
336 338 """return a DirstateItem for the associated path"""
337 339 entry = self._map.get(path)
338 340 if entry is None:
339 341 return DirstateItem()
340 342 return entry
341 343
342 344 def __contains__(self, key):
343 345 return key in self._map
344 346
345 347 def __iter__(self):
346 348 return iter(sorted(self._map))
347 349
348 350 def items(self):
349 351 return pycompat.iteritems(self._map)
350 352
351 353 iteritems = items
352 354
353 355 def parents(self):
354 356 return [self._validate(p) for p in self._pl]
355 357
356 358 def p1(self):
357 359 return self._validate(self._pl[0])
358 360
359 361 def p2(self):
360 362 return self._validate(self._pl[1])
361 363
362 364 @property
363 365 def in_merge(self):
364 366 """True if a merge is in progress"""
365 367 return self._pl[1] != self._nodeconstants.nullid
366 368
367 369 def branch(self):
368 370 return encoding.tolocal(self._branch)
369 371
370 372 def setparents(self, p1, p2=None):
371 373 """Set dirstate parents to p1 and p2.
372 374
373 375 When moving from two parents to one, "merged" entries a
374 376 adjusted to normal and previous copy records discarded and
375 377 returned by the call.
376 378
377 379 See localrepo.setparents()
378 380 """
379 381 if p2 is None:
380 382 p2 = self._nodeconstants.nullid
381 383 if self._parentwriters == 0:
382 384 raise ValueError(
383 385 b"cannot set dirstate parent outside of "
384 386 b"dirstate.parentchange context manager"
385 387 )
386 388
387 389 self._dirty = True
388 390 oldp2 = self._pl[1]
389 391 if self._origpl is None:
390 392 self._origpl = self._pl
391 393 nullid = self._nodeconstants.nullid
392 394 # True if we need to fold p2 related state back to a linear case
393 395 fold_p2 = oldp2 != nullid and p2 == nullid
394 396 return self._map.setparents(p1, p2, fold_p2=fold_p2)
395 397
396 398 def setbranch(self, branch):
397 399 self.__class__._branch.set(self, encoding.fromlocal(branch))
398 400 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
399 401 try:
400 402 f.write(self._branch + b'\n')
401 403 f.close()
402 404
403 405 # make sure filecache has the correct stat info for _branch after
404 406 # replacing the underlying file
405 407 ce = self._filecache[b'_branch']
406 408 if ce:
407 409 ce.refresh()
408 410 except: # re-raises
409 411 f.discard()
410 412 raise
411 413
412 414 def invalidate(self):
413 415 """Causes the next access to reread the dirstate.
414 416
415 417 This is different from localrepo.invalidatedirstate() because it always
416 418 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
417 419 check whether the dirstate has changed before rereading it."""
418 420
419 421 for a in ("_map", "_branch", "_ignore"):
420 422 if a in self.__dict__:
421 423 delattr(self, a)
422 424 self._lastnormaltime = 0
423 425 self._dirty = False
424 426 self._parentwriters = 0
425 427 self._origpl = None
426 428
427 429 def copy(self, source, dest):
428 430 """Mark dest as a copy of source. Unmark dest if source is None."""
429 431 if source == dest:
430 432 return
431 433 self._dirty = True
432 434 if source is not None:
433 435 self._map.copymap[dest] = source
434 436 else:
435 437 self._map.copymap.pop(dest, None)
436 438
437 439 def copied(self, file):
438 440 return self._map.copymap.get(file, None)
439 441
440 442 def copies(self):
441 443 return self._map.copymap
442 444
443 445 @requires_no_parents_change
444 446 def set_tracked(self, filename):
445 447 """a "public" method for generic code to mark a file as tracked
446 448
447 449 This function is to be called outside of "update/merge" case. For
448 450 example by a command like `hg add X`.
449 451
450 452 return True the file was previously untracked, False otherwise.
451 453 """
452 454 self._dirty = True
453 455 entry = self._map.get(filename)
454 456 if entry is None or not entry.tracked:
455 457 self._check_new_tracked_filename(filename)
456 458 return self._map.set_tracked(filename)
457 459
458 460 @requires_no_parents_change
459 461 def set_untracked(self, filename):
460 462 """a "public" method for generic code to mark a file as untracked
461 463
462 464 This function is to be called outside of "update/merge" case. For
463 465 example by a command like `hg remove X`.
464 466
465 467 return True the file was previously tracked, False otherwise.
466 468 """
467 469 ret = self._map.set_untracked(filename)
468 470 if ret:
469 471 self._dirty = True
470 472 return ret
471 473
472 474 @requires_no_parents_change
473 475 def set_clean(self, filename, parentfiledata=None):
474 476 """record that the current state of the file on disk is known to be clean"""
475 477 self._dirty = True
476 478 if parentfiledata:
477 479 (mode, size, mtime) = parentfiledata
478 480 else:
479 481 (mode, size, mtime) = self._get_filedata(filename)
480 482 if not self._map[filename].tracked:
481 483 self._check_new_tracked_filename(filename)
482 484 self._map.set_clean(filename, mode, size, mtime)
483 485 if mtime > self._lastnormaltime:
484 486 # Remember the most recent modification timeslot for status(),
485 487 # to make sure we won't miss future size-preserving file content
486 488 # modifications that happen within the same timeslot.
487 489 self._lastnormaltime = mtime
488 490
489 491 @requires_no_parents_change
490 492 def set_possibly_dirty(self, filename):
491 493 """record that the current state of the file on disk is unknown"""
492 494 self._dirty = True
493 495 self._map.set_possibly_dirty(filename)
494 496
495 497 @requires_parents_change
496 498 def update_file_p1(
497 499 self,
498 500 filename,
499 501 p1_tracked,
500 502 ):
501 503 """Set a file as tracked in the parent (or not)
502 504
503 505 This is to be called when adjust the dirstate to a new parent after an history
504 506 rewriting operation.
505 507
506 508 It should not be called during a merge (p2 != nullid) and only within
507 509 a `with dirstate.parentchange():` context.
508 510 """
509 511 if self.in_merge:
510 512 msg = b'update_file_reference should not be called when merging'
511 513 raise error.ProgrammingError(msg)
512 514 entry = self._map.get(filename)
513 515 if entry is None:
514 516 wc_tracked = False
515 517 else:
516 518 wc_tracked = entry.tracked
517 519 possibly_dirty = False
518 520 if p1_tracked and wc_tracked:
519 521 # the underlying reference might have changed, we will have to
520 522 # check it.
521 523 possibly_dirty = True
522 524 elif not (p1_tracked or wc_tracked):
523 525 # the file is no longer relevant to anyone
524 526 if self._map.get(filename) is not None:
525 527 self._map.reset_state(filename)
526 528 self._dirty = True
527 529 elif (not p1_tracked) and wc_tracked:
528 530 if entry is not None and entry.added:
529 531 return # avoid dropping copy information (maybe?)
530 532 elif p1_tracked and not wc_tracked:
531 533 pass
532 534 else:
533 535 assert False, 'unreachable'
534 536
535 537 # this mean we are doing call for file we do not really care about the
536 538 # data (eg: added or removed), however this should be a minor overhead
537 539 # compared to the overall update process calling this.
538 540 parentfiledata = None
539 541 if wc_tracked:
540 542 parentfiledata = self._get_filedata(filename)
541 543
542 544 self._map.reset_state(
543 545 filename,
544 546 wc_tracked,
545 547 p1_tracked,
546 548 possibly_dirty=possibly_dirty,
547 549 parentfiledata=parentfiledata,
548 550 )
549 551 if (
550 552 parentfiledata is not None
551 553 and parentfiledata[2] > self._lastnormaltime
552 554 ):
553 555 # Remember the most recent modification timeslot for status(),
554 556 # to make sure we won't miss future size-preserving file content
555 557 # modifications that happen within the same timeslot.
556 558 self._lastnormaltime = parentfiledata[2]
557 559
558 560 @requires_parents_change
559 561 def update_file(
560 562 self,
561 563 filename,
562 564 wc_tracked,
563 565 p1_tracked,
564 566 p2_tracked=False,
565 567 merged=False,
566 568 clean_p1=False,
567 569 clean_p2=False,
568 570 possibly_dirty=False,
569 571 parentfiledata=None,
570 572 ):
571 573 """update the information about a file in the dirstate
572 574
573 575 This is to be called when the direstates parent changes to keep track
574 576 of what is the file situation in regards to the working copy and its parent.
575 577
576 578 This function must be called within a `dirstate.parentchange` context.
577 579
578 580 note: the API is at an early stage and we might need to adjust it
579 581 depending of what information ends up being relevant and useful to
580 582 other processing.
581 583 """
582 584 if merged and (clean_p1 or clean_p2):
583 585 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
584 586 raise error.ProgrammingError(msg)
585 587
586 588 # note: I do not think we need to double check name clash here since we
587 589 # are in a update/merge case that should already have taken care of
588 590 # this. The test agrees
589 591
590 592 self._dirty = True
591 593
592 594 need_parent_file_data = (
593 595 not (possibly_dirty or clean_p2 or merged)
594 596 and wc_tracked
595 597 and p1_tracked
596 598 )
597 599
598 600 # this mean we are doing call for file we do not really care about the
599 601 # data (eg: added or removed), however this should be a minor overhead
600 602 # compared to the overall update process calling this.
601 603 if need_parent_file_data:
602 604 if parentfiledata is None:
603 605 parentfiledata = self._get_filedata(filename)
604 606 mtime = parentfiledata[2]
605 607
606 608 if mtime > self._lastnormaltime:
607 609 # Remember the most recent modification timeslot for
608 610 # status(), to make sure we won't miss future
609 611 # size-preserving file content modifications that happen
610 612 # within the same timeslot.
611 613 self._lastnormaltime = mtime
612 614
613 615 self._map.reset_state(
614 616 filename,
615 617 wc_tracked,
616 618 p1_tracked,
617 619 p2_tracked=p2_tracked,
618 620 merged=merged,
619 621 clean_p1=clean_p1,
620 622 clean_p2=clean_p2,
621 623 possibly_dirty=possibly_dirty,
622 624 parentfiledata=parentfiledata,
623 625 )
624 626 if (
625 627 parentfiledata is not None
626 628 and parentfiledata[2] > self._lastnormaltime
627 629 ):
628 630 # Remember the most recent modification timeslot for status(),
629 631 # to make sure we won't miss future size-preserving file content
630 632 # modifications that happen within the same timeslot.
631 633 self._lastnormaltime = parentfiledata[2]
632 634
633 635 def _check_new_tracked_filename(self, filename):
634 636 scmutil.checkfilename(filename)
635 637 if self._map.hastrackeddir(filename):
636 638 msg = _(b'directory %r already in dirstate')
637 639 msg %= pycompat.bytestr(filename)
638 640 raise error.Abort(msg)
639 641 # shadows
640 642 for d in pathutil.finddirs(filename):
641 643 if self._map.hastrackeddir(d):
642 644 break
643 645 entry = self._map.get(d)
644 646 if entry is not None and not entry.removed:
645 647 msg = _(b'file %r in dirstate clashes with %r')
646 648 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
647 649 raise error.Abort(msg)
648 650
649 651 def _get_filedata(self, filename):
650 652 """returns"""
651 653 s = os.lstat(self._join(filename))
652 654 mode = s.st_mode
653 655 size = s.st_size
654 656 mtime = s[stat.ST_MTIME]
655 657 return (mode, size, mtime)
656 658
657 659 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
658 660 if exists is None:
659 661 exists = os.path.lexists(os.path.join(self._root, path))
660 662 if not exists:
661 663 # Maybe a path component exists
662 664 if not ignoremissing and b'/' in path:
663 665 d, f = path.rsplit(b'/', 1)
664 666 d = self._normalize(d, False, ignoremissing, None)
665 667 folded = d + b"/" + f
666 668 else:
667 669 # No path components, preserve original case
668 670 folded = path
669 671 else:
670 672 # recursively normalize leading directory components
671 673 # against dirstate
672 674 if b'/' in normed:
673 675 d, f = normed.rsplit(b'/', 1)
674 676 d = self._normalize(d, False, ignoremissing, True)
675 677 r = self._root + b"/" + d
676 678 folded = d + b"/" + util.fspath(f, r)
677 679 else:
678 680 folded = util.fspath(normed, self._root)
679 681 storemap[normed] = folded
680 682
681 683 return folded
682 684
683 685 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
684 686 normed = util.normcase(path)
685 687 folded = self._map.filefoldmap.get(normed, None)
686 688 if folded is None:
687 689 if isknown:
688 690 folded = path
689 691 else:
690 692 folded = self._discoverpath(
691 693 path, normed, ignoremissing, exists, self._map.filefoldmap
692 694 )
693 695 return folded
694 696
695 697 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
696 698 normed = util.normcase(path)
697 699 folded = self._map.filefoldmap.get(normed, None)
698 700 if folded is None:
699 701 folded = self._map.dirfoldmap.get(normed, None)
700 702 if folded is None:
701 703 if isknown:
702 704 folded = path
703 705 else:
704 706 # store discovered result in dirfoldmap so that future
705 707 # normalizefile calls don't start matching directories
706 708 folded = self._discoverpath(
707 709 path, normed, ignoremissing, exists, self._map.dirfoldmap
708 710 )
709 711 return folded
710 712
711 713 def normalize(self, path, isknown=False, ignoremissing=False):
712 714 """
713 715 normalize the case of a pathname when on a casefolding filesystem
714 716
715 717 isknown specifies whether the filename came from walking the
716 718 disk, to avoid extra filesystem access.
717 719
718 720 If ignoremissing is True, missing path are returned
719 721 unchanged. Otherwise, we try harder to normalize possibly
720 722 existing path components.
721 723
722 724 The normalized case is determined based on the following precedence:
723 725
724 726 - version of name already stored in the dirstate
725 727 - version of name stored on disk
726 728 - version provided via command arguments
727 729 """
728 730
729 731 if self._checkcase:
730 732 return self._normalize(path, isknown, ignoremissing)
731 733 return path
732 734
733 735 def clear(self):
734 736 self._map.clear()
735 737 self._lastnormaltime = 0
736 738 self._dirty = True
737 739
738 740 def rebuild(self, parent, allfiles, changedfiles=None):
739 741 if changedfiles is None:
740 742 # Rebuild entire dirstate
741 743 to_lookup = allfiles
742 744 to_drop = []
743 745 lastnormaltime = self._lastnormaltime
744 746 self.clear()
745 747 self._lastnormaltime = lastnormaltime
746 748 elif len(changedfiles) < 10:
747 749 # Avoid turning allfiles into a set, which can be expensive if it's
748 750 # large.
749 751 to_lookup = []
750 752 to_drop = []
751 753 for f in changedfiles:
752 754 if f in allfiles:
753 755 to_lookup.append(f)
754 756 else:
755 757 to_drop.append(f)
756 758 else:
757 759 changedfilesset = set(changedfiles)
758 760 to_lookup = changedfilesset & set(allfiles)
759 761 to_drop = changedfilesset - to_lookup
760 762
761 763 if self._origpl is None:
762 764 self._origpl = self._pl
763 765 self._map.setparents(parent, self._nodeconstants.nullid)
764 766
765 767 for f in to_lookup:
766 768
767 769 if self.in_merge:
768 770 self.set_tracked(f)
769 771 else:
770 772 self._map.reset_state(
771 773 f,
772 774 wc_tracked=True,
773 775 p1_tracked=True,
774 776 possibly_dirty=True,
775 777 )
776 778 for f in to_drop:
777 779 self._map.reset_state(f)
778 780
779 781 self._dirty = True
780 782
781 783 def identity(self):
782 784 """Return identity of dirstate itself to detect changing in storage
783 785
784 786 If identity of previous dirstate is equal to this, writing
785 787 changes based on the former dirstate out can keep consistency.
786 788 """
787 789 return self._map.identity
788 790
789 791 def write(self, tr):
790 792 if not self._dirty:
791 793 return
792 794
793 795 filename = self._filename
794 796 if tr:
795 797 # 'dirstate.write()' is not only for writing in-memory
796 798 # changes out, but also for dropping ambiguous timestamp.
797 799 # delayed writing re-raise "ambiguous timestamp issue".
798 800 # See also the wiki page below for detail:
799 801 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
800 802
801 803 # record when mtime start to be ambiguous
802 804 now = _getfsnow(self._opener)
803 805
804 806 # delay writing in-memory changes out
805 807 tr.addfilegenerator(
806 808 b'dirstate',
807 809 (self._filename,),
808 810 lambda f: self._writedirstate(tr, f, now=now),
809 811 location=b'plain',
810 812 )
811 813 return
812 814
813 815 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
814 816 self._writedirstate(tr, st)
815 817
816 818 def addparentchangecallback(self, category, callback):
817 819 """add a callback to be called when the wd parents are changed
818 820
819 821 Callback will be called with the following arguments:
820 822 dirstate, (oldp1, oldp2), (newp1, newp2)
821 823
822 824 Category is a unique identifier to allow overwriting an old callback
823 825 with a newer callback.
824 826 """
825 827 self._plchangecallbacks[category] = callback
826 828
827 829 def _writedirstate(self, tr, st, now=None):
828 830 # notify callbacks about parents change
829 831 if self._origpl is not None and self._origpl != self._pl:
830 832 for c, callback in sorted(
831 833 pycompat.iteritems(self._plchangecallbacks)
832 834 ):
833 835 callback(self, self._origpl, self._pl)
834 836 self._origpl = None
835 837
836 838 if now is None:
837 839 # use the modification time of the newly created temporary file as the
838 840 # filesystem's notion of 'now'
839 841 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
840 842
841 843 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
842 844 # timestamp of each entries in dirstate, because of 'now > mtime'
843 845 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
844 846 if delaywrite > 0:
845 847 # do we have any files to delay for?
846 848 for f, e in pycompat.iteritems(self._map):
847 849 if e.need_delay(now):
848 850 import time # to avoid useless import
849 851
850 852 # rather than sleep n seconds, sleep until the next
851 853 # multiple of n seconds
852 854 clock = time.time()
853 855 start = int(clock) - (int(clock) % delaywrite)
854 856 end = start + delaywrite
855 857 time.sleep(end - clock)
856 858 now = end # trust our estimate that the end is near now
857 859 break
858 860
859 861 self._map.write(tr, st, now)
860 862 self._lastnormaltime = 0
861 863 self._dirty = False
862 864
863 865 def _dirignore(self, f):
864 866 if self._ignore(f):
865 867 return True
866 868 for p in pathutil.finddirs(f):
867 869 if self._ignore(p):
868 870 return True
869 871 return False
870 872
871 873 def _ignorefiles(self):
872 874 files = []
873 875 if os.path.exists(self._join(b'.hgignore')):
874 876 files.append(self._join(b'.hgignore'))
875 877 for name, path in self._ui.configitems(b"ui"):
876 878 if name == b'ignore' or name.startswith(b'ignore.'):
877 879 # we need to use os.path.join here rather than self._join
878 880 # because path is arbitrary and user-specified
879 881 files.append(os.path.join(self._rootdir, util.expandpath(path)))
880 882 return files
881 883
882 884 def _ignorefileandline(self, f):
883 885 files = collections.deque(self._ignorefiles())
884 886 visited = set()
885 887 while files:
886 888 i = files.popleft()
887 889 patterns = matchmod.readpatternfile(
888 890 i, self._ui.warn, sourceinfo=True
889 891 )
890 892 for pattern, lineno, line in patterns:
891 893 kind, p = matchmod._patsplit(pattern, b'glob')
892 894 if kind == b"subinclude":
893 895 if p not in visited:
894 896 files.append(p)
895 897 continue
896 898 m = matchmod.match(
897 899 self._root, b'', [], [pattern], warn=self._ui.warn
898 900 )
899 901 if m(f):
900 902 return (i, lineno, line)
901 903 visited.add(i)
902 904 return (None, -1, b"")
903 905
904 906 def _walkexplicit(self, match, subrepos):
905 907 """Get stat data about the files explicitly specified by match.
906 908
907 909 Return a triple (results, dirsfound, dirsnotfound).
908 910 - results is a mapping from filename to stat result. It also contains
909 911 listings mapping subrepos and .hg to None.
910 912 - dirsfound is a list of files found to be directories.
911 913 - dirsnotfound is a list of files that the dirstate thinks are
912 914 directories and that were not found."""
913 915
914 916 def badtype(mode):
915 917 kind = _(b'unknown')
916 918 if stat.S_ISCHR(mode):
917 919 kind = _(b'character device')
918 920 elif stat.S_ISBLK(mode):
919 921 kind = _(b'block device')
920 922 elif stat.S_ISFIFO(mode):
921 923 kind = _(b'fifo')
922 924 elif stat.S_ISSOCK(mode):
923 925 kind = _(b'socket')
924 926 elif stat.S_ISDIR(mode):
925 927 kind = _(b'directory')
926 928 return _(b'unsupported file type (type is %s)') % kind
927 929
928 930 badfn = match.bad
929 931 dmap = self._map
930 932 lstat = os.lstat
931 933 getkind = stat.S_IFMT
932 934 dirkind = stat.S_IFDIR
933 935 regkind = stat.S_IFREG
934 936 lnkkind = stat.S_IFLNK
935 937 join = self._join
936 938 dirsfound = []
937 939 foundadd = dirsfound.append
938 940 dirsnotfound = []
939 941 notfoundadd = dirsnotfound.append
940 942
941 943 if not match.isexact() and self._checkcase:
942 944 normalize = self._normalize
943 945 else:
944 946 normalize = None
945 947
946 948 files = sorted(match.files())
947 949 subrepos.sort()
948 950 i, j = 0, 0
949 951 while i < len(files) and j < len(subrepos):
950 952 subpath = subrepos[j] + b"/"
951 953 if files[i] < subpath:
952 954 i += 1
953 955 continue
954 956 while i < len(files) and files[i].startswith(subpath):
955 957 del files[i]
956 958 j += 1
957 959
958 960 if not files or b'' in files:
959 961 files = [b'']
960 962 # constructing the foldmap is expensive, so don't do it for the
961 963 # common case where files is ['']
962 964 normalize = None
963 965 results = dict.fromkeys(subrepos)
964 966 results[b'.hg'] = None
965 967
966 968 for ff in files:
967 969 if normalize:
968 970 nf = normalize(ff, False, True)
969 971 else:
970 972 nf = ff
971 973 if nf in results:
972 974 continue
973 975
974 976 try:
975 977 st = lstat(join(nf))
976 978 kind = getkind(st.st_mode)
977 979 if kind == dirkind:
978 980 if nf in dmap:
979 981 # file replaced by dir on disk but still in dirstate
980 982 results[nf] = None
981 983 foundadd((nf, ff))
982 984 elif kind == regkind or kind == lnkkind:
983 985 results[nf] = st
984 986 else:
985 987 badfn(ff, badtype(kind))
986 988 if nf in dmap:
987 989 results[nf] = None
988 990 except OSError as inst: # nf not found on disk - it is dirstate only
989 991 if nf in dmap: # does it exactly match a missing file?
990 992 results[nf] = None
991 993 else: # does it match a missing directory?
992 994 if self._map.hasdir(nf):
993 995 notfoundadd(nf)
994 996 else:
995 997 badfn(ff, encoding.strtolocal(inst.strerror))
996 998
997 999 # match.files() may contain explicitly-specified paths that shouldn't
998 1000 # be taken; drop them from the list of files found. dirsfound/notfound
999 1001 # aren't filtered here because they will be tested later.
1000 1002 if match.anypats():
1001 1003 for f in list(results):
1002 1004 if f == b'.hg' or f in subrepos:
1003 1005 # keep sentinel to disable further out-of-repo walks
1004 1006 continue
1005 1007 if not match(f):
1006 1008 del results[f]
1007 1009
1008 1010 # Case insensitive filesystems cannot rely on lstat() failing to detect
1009 1011 # a case-only rename. Prune the stat object for any file that does not
1010 1012 # match the case in the filesystem, if there are multiple files that
1011 1013 # normalize to the same path.
1012 1014 if match.isexact() and self._checkcase:
1013 1015 normed = {}
1014 1016
1015 1017 for f, st in pycompat.iteritems(results):
1016 1018 if st is None:
1017 1019 continue
1018 1020
1019 1021 nc = util.normcase(f)
1020 1022 paths = normed.get(nc)
1021 1023
1022 1024 if paths is None:
1023 1025 paths = set()
1024 1026 normed[nc] = paths
1025 1027
1026 1028 paths.add(f)
1027 1029
1028 1030 for norm, paths in pycompat.iteritems(normed):
1029 1031 if len(paths) > 1:
1030 1032 for path in paths:
1031 1033 folded = self._discoverpath(
1032 1034 path, norm, True, None, self._map.dirfoldmap
1033 1035 )
1034 1036 if path != folded:
1035 1037 results[path] = None
1036 1038
1037 1039 return results, dirsfound, dirsnotfound
1038 1040
1039 1041 def walk(self, match, subrepos, unknown, ignored, full=True):
1040 1042 """
1041 1043 Walk recursively through the directory tree, finding all files
1042 1044 matched by match.
1043 1045
1044 1046 If full is False, maybe skip some known-clean files.
1045 1047
1046 1048 Return a dict mapping filename to stat-like object (either
1047 1049 mercurial.osutil.stat instance or return value of os.stat()).
1048 1050
1049 1051 """
1050 1052 # full is a flag that extensions that hook into walk can use -- this
1051 1053 # implementation doesn't use it at all. This satisfies the contract
1052 1054 # because we only guarantee a "maybe".
1053 1055
1054 1056 if ignored:
1055 1057 ignore = util.never
1056 1058 dirignore = util.never
1057 1059 elif unknown:
1058 1060 ignore = self._ignore
1059 1061 dirignore = self._dirignore
1060 1062 else:
1061 1063 # if not unknown and not ignored, drop dir recursion and step 2
1062 1064 ignore = util.always
1063 1065 dirignore = util.always
1064 1066
1065 1067 matchfn = match.matchfn
1066 1068 matchalways = match.always()
1067 1069 matchtdir = match.traversedir
1068 1070 dmap = self._map
1069 1071 listdir = util.listdir
1070 1072 lstat = os.lstat
1071 1073 dirkind = stat.S_IFDIR
1072 1074 regkind = stat.S_IFREG
1073 1075 lnkkind = stat.S_IFLNK
1074 1076 join = self._join
1075 1077
1076 1078 exact = skipstep3 = False
1077 1079 if match.isexact(): # match.exact
1078 1080 exact = True
1079 1081 dirignore = util.always # skip step 2
1080 1082 elif match.prefix(): # match.match, no patterns
1081 1083 skipstep3 = True
1082 1084
1083 1085 if not exact and self._checkcase:
1084 1086 normalize = self._normalize
1085 1087 normalizefile = self._normalizefile
1086 1088 skipstep3 = False
1087 1089 else:
1088 1090 normalize = self._normalize
1089 1091 normalizefile = None
1090 1092
1091 1093 # step 1: find all explicit files
1092 1094 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1093 1095 if matchtdir:
1094 1096 for d in work:
1095 1097 matchtdir(d[0])
1096 1098 for d in dirsnotfound:
1097 1099 matchtdir(d)
1098 1100
1099 1101 skipstep3 = skipstep3 and not (work or dirsnotfound)
1100 1102 work = [d for d in work if not dirignore(d[0])]
1101 1103
1102 1104 # step 2: visit subdirectories
1103 1105 def traverse(work, alreadynormed):
1104 1106 wadd = work.append
1105 1107 while work:
1106 1108 tracing.counter('dirstate.walk work', len(work))
1107 1109 nd = work.pop()
1108 1110 visitentries = match.visitchildrenset(nd)
1109 1111 if not visitentries:
1110 1112 continue
1111 1113 if visitentries == b'this' or visitentries == b'all':
1112 1114 visitentries = None
1113 1115 skip = None
1114 1116 if nd != b'':
1115 1117 skip = b'.hg'
1116 1118 try:
1117 1119 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1118 1120 entries = listdir(join(nd), stat=True, skip=skip)
1119 1121 except OSError as inst:
1120 1122 if inst.errno in (errno.EACCES, errno.ENOENT):
1121 1123 match.bad(
1122 1124 self.pathto(nd), encoding.strtolocal(inst.strerror)
1123 1125 )
1124 1126 continue
1125 1127 raise
1126 1128 for f, kind, st in entries:
1127 1129 # Some matchers may return files in the visitentries set,
1128 1130 # instead of 'this', if the matcher explicitly mentions them
1129 1131 # and is not an exactmatcher. This is acceptable; we do not
1130 1132 # make any hard assumptions about file-or-directory below
1131 1133 # based on the presence of `f` in visitentries. If
1132 1134 # visitchildrenset returned a set, we can always skip the
1133 1135 # entries *not* in the set it provided regardless of whether
1134 1136 # they're actually a file or a directory.
1135 1137 if visitentries and f not in visitentries:
1136 1138 continue
1137 1139 if normalizefile:
1138 1140 # even though f might be a directory, we're only
1139 1141 # interested in comparing it to files currently in the
1140 1142 # dmap -- therefore normalizefile is enough
1141 1143 nf = normalizefile(
1142 1144 nd and (nd + b"/" + f) or f, True, True
1143 1145 )
1144 1146 else:
1145 1147 nf = nd and (nd + b"/" + f) or f
1146 1148 if nf not in results:
1147 1149 if kind == dirkind:
1148 1150 if not ignore(nf):
1149 1151 if matchtdir:
1150 1152 matchtdir(nf)
1151 1153 wadd(nf)
1152 1154 if nf in dmap and (matchalways or matchfn(nf)):
1153 1155 results[nf] = None
1154 1156 elif kind == regkind or kind == lnkkind:
1155 1157 if nf in dmap:
1156 1158 if matchalways or matchfn(nf):
1157 1159 results[nf] = st
1158 1160 elif (matchalways or matchfn(nf)) and not ignore(
1159 1161 nf
1160 1162 ):
1161 1163 # unknown file -- normalize if necessary
1162 1164 if not alreadynormed:
1163 1165 nf = normalize(nf, False, True)
1164 1166 results[nf] = st
1165 1167 elif nf in dmap and (matchalways or matchfn(nf)):
1166 1168 results[nf] = None
1167 1169
1168 1170 for nd, d in work:
1169 1171 # alreadynormed means that processwork doesn't have to do any
1170 1172 # expensive directory normalization
1171 1173 alreadynormed = not normalize or nd == d
1172 1174 traverse([d], alreadynormed)
1173 1175
1174 1176 for s in subrepos:
1175 1177 del results[s]
1176 1178 del results[b'.hg']
1177 1179
1178 1180 # step 3: visit remaining files from dmap
1179 1181 if not skipstep3 and not exact:
1180 1182 # If a dmap file is not in results yet, it was either
1181 1183 # a) not matching matchfn b) ignored, c) missing, or d) under a
1182 1184 # symlink directory.
1183 1185 if not results and matchalways:
1184 1186 visit = [f for f in dmap]
1185 1187 else:
1186 1188 visit = [f for f in dmap if f not in results and matchfn(f)]
1187 1189 visit.sort()
1188 1190
1189 1191 if unknown:
1190 1192 # unknown == True means we walked all dirs under the roots
1191 1193 # that wasn't ignored, and everything that matched was stat'ed
1192 1194 # and is already in results.
1193 1195 # The rest must thus be ignored or under a symlink.
1194 1196 audit_path = pathutil.pathauditor(self._root, cached=True)
1195 1197
1196 1198 for nf in iter(visit):
1197 1199 # If a stat for the same file was already added with a
1198 1200 # different case, don't add one for this, since that would
1199 1201 # make it appear as if the file exists under both names
1200 1202 # on disk.
1201 1203 if (
1202 1204 normalizefile
1203 1205 and normalizefile(nf, True, True) in results
1204 1206 ):
1205 1207 results[nf] = None
1206 1208 # Report ignored items in the dmap as long as they are not
1207 1209 # under a symlink directory.
1208 1210 elif audit_path.check(nf):
1209 1211 try:
1210 1212 results[nf] = lstat(join(nf))
1211 1213 # file was just ignored, no links, and exists
1212 1214 except OSError:
1213 1215 # file doesn't exist
1214 1216 results[nf] = None
1215 1217 else:
1216 1218 # It's either missing or under a symlink directory
1217 1219 # which we in this case report as missing
1218 1220 results[nf] = None
1219 1221 else:
1220 1222 # We may not have walked the full directory tree above,
1221 1223 # so stat and check everything we missed.
1222 1224 iv = iter(visit)
1223 1225 for st in util.statfiles([join(i) for i in visit]):
1224 1226 results[next(iv)] = st
1225 1227 return results
1226 1228
1227 1229 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1228 1230 # Force Rayon (Rust parallelism library) to respect the number of
1229 1231 # workers. This is a temporary workaround until Rust code knows
1230 1232 # how to read the config file.
1231 1233 numcpus = self._ui.configint(b"worker", b"numcpus")
1232 1234 if numcpus is not None:
1233 1235 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1234 1236
1235 1237 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1236 1238 if not workers_enabled:
1237 1239 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1238 1240
1239 1241 (
1240 1242 lookup,
1241 1243 modified,
1242 1244 added,
1243 1245 removed,
1244 1246 deleted,
1245 1247 clean,
1246 1248 ignored,
1247 1249 unknown,
1248 1250 warnings,
1249 1251 bad,
1250 1252 traversed,
1251 1253 dirty,
1252 1254 ) = rustmod.status(
1253 1255 self._map._rustmap,
1254 1256 matcher,
1255 1257 self._rootdir,
1256 1258 self._ignorefiles(),
1257 1259 self._checkexec,
1258 1260 self._lastnormaltime,
1259 1261 bool(list_clean),
1260 1262 bool(list_ignored),
1261 1263 bool(list_unknown),
1262 1264 bool(matcher.traversedir),
1263 1265 )
1264 1266
1265 1267 self._dirty |= dirty
1266 1268
1267 1269 if matcher.traversedir:
1268 1270 for dir in traversed:
1269 1271 matcher.traversedir(dir)
1270 1272
1271 1273 if self._ui.warn:
1272 1274 for item in warnings:
1273 1275 if isinstance(item, tuple):
1274 1276 file_path, syntax = item
1275 1277 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1276 1278 file_path,
1277 1279 syntax,
1278 1280 )
1279 1281 self._ui.warn(msg)
1280 1282 else:
1281 1283 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1282 1284 self._ui.warn(
1283 1285 msg
1284 1286 % (
1285 1287 pathutil.canonpath(
1286 1288 self._rootdir, self._rootdir, item
1287 1289 ),
1288 1290 b"No such file or directory",
1289 1291 )
1290 1292 )
1291 1293
1292 1294 for (fn, message) in bad:
1293 1295 matcher.bad(fn, encoding.strtolocal(message))
1294 1296
1295 1297 status = scmutil.status(
1296 1298 modified=modified,
1297 1299 added=added,
1298 1300 removed=removed,
1299 1301 deleted=deleted,
1300 1302 unknown=unknown,
1301 1303 ignored=ignored,
1302 1304 clean=clean,
1303 1305 )
1304 1306 return (lookup, status)
1305 1307
1306 1308 def status(self, match, subrepos, ignored, clean, unknown):
1307 1309 """Determine the status of the working copy relative to the
1308 1310 dirstate and return a pair of (unsure, status), where status is of type
1309 1311 scmutil.status and:
1310 1312
1311 1313 unsure:
1312 1314 files that might have been modified since the dirstate was
1313 1315 written, but need to be read to be sure (size is the same
1314 1316 but mtime differs)
1315 1317 status.modified:
1316 1318 files that have definitely been modified since the dirstate
1317 1319 was written (different size or mode)
1318 1320 status.clean:
1319 1321 files that have definitely not been modified since the
1320 1322 dirstate was written
1321 1323 """
1322 1324 listignored, listclean, listunknown = ignored, clean, unknown
1323 1325 lookup, modified, added, unknown, ignored = [], [], [], [], []
1324 1326 removed, deleted, clean = [], [], []
1325 1327
1326 1328 dmap = self._map
1327 1329 dmap.preload()
1328 1330
1329 1331 use_rust = True
1330 1332
1331 1333 allowed_matchers = (
1332 1334 matchmod.alwaysmatcher,
1333 1335 matchmod.exactmatcher,
1334 1336 matchmod.includematcher,
1335 1337 )
1336 1338
1337 1339 if rustmod is None:
1338 1340 use_rust = False
1339 1341 elif self._checkcase:
1340 1342 # Case-insensitive filesystems are not handled yet
1341 1343 use_rust = False
1342 1344 elif subrepos:
1343 1345 use_rust = False
1344 1346 elif sparse.enabled:
1345 1347 use_rust = False
1346 1348 elif not isinstance(match, allowed_matchers):
1347 1349 # Some matchers have yet to be implemented
1348 1350 use_rust = False
1349 1351
1350 1352 if use_rust:
1351 1353 try:
1352 1354 return self._rust_status(
1353 1355 match, listclean, listignored, listunknown
1354 1356 )
1355 1357 except rustmod.FallbackError:
1356 1358 pass
1357 1359
1358 1360 def noop(f):
1359 1361 pass
1360 1362
1361 1363 dcontains = dmap.__contains__
1362 1364 dget = dmap.__getitem__
1363 1365 ladd = lookup.append # aka "unsure"
1364 1366 madd = modified.append
1365 1367 aadd = added.append
1366 1368 uadd = unknown.append if listunknown else noop
1367 1369 iadd = ignored.append if listignored else noop
1368 1370 radd = removed.append
1369 1371 dadd = deleted.append
1370 1372 cadd = clean.append if listclean else noop
1371 1373 mexact = match.exact
1372 1374 dirignore = self._dirignore
1373 1375 checkexec = self._checkexec
1374 1376 copymap = self._map.copymap
1375 1377 lastnormaltime = self._lastnormaltime
1376 1378
1377 1379 # We need to do full walks when either
1378 1380 # - we're listing all clean files, or
1379 1381 # - match.traversedir does something, because match.traversedir should
1380 1382 # be called for every dir in the working dir
1381 1383 full = listclean or match.traversedir is not None
1382 1384 for fn, st in pycompat.iteritems(
1383 1385 self.walk(match, subrepos, listunknown, listignored, full=full)
1384 1386 ):
1385 1387 if not dcontains(fn):
1386 1388 if (listignored or mexact(fn)) and dirignore(fn):
1387 1389 if listignored:
1388 1390 iadd(fn)
1389 1391 else:
1390 1392 uadd(fn)
1391 1393 continue
1392 1394
1393 1395 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1394 1396 # written like that for performance reasons. dmap[fn] is not a
1395 1397 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1396 1398 # opcode has fast paths when the value to be unpacked is a tuple or
1397 1399 # a list, but falls back to creating a full-fledged iterator in
1398 1400 # general. That is much slower than simply accessing and storing the
1399 1401 # tuple members one by one.
1400 1402 t = dget(fn)
1401 1403 mode = t.mode
1402 1404 size = t.size
1403 1405 time = t.mtime
1404 1406
1405 1407 if not st and t.tracked:
1406 1408 dadd(fn)
1407 1409 elif t.merged:
1408 1410 madd(fn)
1409 1411 elif t.added:
1410 1412 aadd(fn)
1411 1413 elif t.removed:
1412 1414 radd(fn)
1413 1415 elif t.tracked:
1414 1416 if (
1415 1417 size >= 0
1416 1418 and (
1417 1419 (size != st.st_size and size != st.st_size & _rangemask)
1418 1420 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1419 1421 )
1420 1422 or t.from_p2
1421 1423 or fn in copymap
1422 1424 ):
1423 1425 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1424 1426 # issue6456: Size returned may be longer due to
1425 1427 # encryption on EXT-4 fscrypt, undecided.
1426 1428 ladd(fn)
1427 1429 else:
1428 1430 madd(fn)
1429 1431 elif (
1430 1432 time != st[stat.ST_MTIME]
1431 1433 and time != st[stat.ST_MTIME] & _rangemask
1432 1434 ):
1433 1435 ladd(fn)
1434 1436 elif st[stat.ST_MTIME] == lastnormaltime:
1435 1437 # fn may have just been marked as normal and it may have
1436 1438 # changed in the same second without changing its size.
1437 1439 # This can happen if we quickly do multiple commits.
1438 1440 # Force lookup, so we don't miss such a racy file change.
1439 1441 ladd(fn)
1440 1442 elif listclean:
1441 1443 cadd(fn)
1442 1444 status = scmutil.status(
1443 1445 modified, added, removed, deleted, unknown, ignored, clean
1444 1446 )
1445 1447 return (lookup, status)
1446 1448
1447 1449 def matches(self, match):
1448 1450 """
1449 1451 return files in the dirstate (in whatever state) filtered by match
1450 1452 """
1451 1453 dmap = self._map
1452 1454 if rustmod is not None:
1453 1455 dmap = self._map._rustmap
1454 1456
1455 1457 if match.always():
1456 1458 return dmap.keys()
1457 1459 files = match.files()
1458 1460 if match.isexact():
1459 1461 # fast path -- filter the other way around, since typically files is
1460 1462 # much smaller than dmap
1461 1463 return [f for f in files if f in dmap]
1462 1464 if match.prefix() and all(fn in dmap for fn in files):
1463 1465 # fast path -- all the values are known to be files, so just return
1464 1466 # that
1465 1467 return list(files)
1466 1468 return [f for f in dmap if match(f)]
1467 1469
1468 1470 def _actualfilename(self, tr):
1469 1471 if tr:
1470 1472 return self._pendingfilename
1471 1473 else:
1472 1474 return self._filename
1473 1475
1474 1476 def savebackup(self, tr, backupname):
1475 1477 '''Save current dirstate into backup file'''
1476 1478 filename = self._actualfilename(tr)
1477 1479 assert backupname != filename
1478 1480
1479 1481 # use '_writedirstate' instead of 'write' to write changes certainly,
1480 1482 # because the latter omits writing out if transaction is running.
1481 1483 # output file will be used to create backup of dirstate at this point.
1482 1484 if self._dirty or not self._opener.exists(filename):
1483 1485 self._writedirstate(
1484 1486 tr,
1485 1487 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1486 1488 )
1487 1489
1488 1490 if tr:
1489 1491 # ensure that subsequent tr.writepending returns True for
1490 1492 # changes written out above, even if dirstate is never
1491 1493 # changed after this
1492 1494 tr.addfilegenerator(
1493 1495 b'dirstate',
1494 1496 (self._filename,),
1495 1497 lambda f: self._writedirstate(tr, f),
1496 1498 location=b'plain',
1497 1499 )
1498 1500
1499 1501 # ensure that pending file written above is unlinked at
1500 1502 # failure, even if tr.writepending isn't invoked until the
1501 1503 # end of this transaction
1502 1504 tr.registertmp(filename, location=b'plain')
1503 1505
1504 1506 self._opener.tryunlink(backupname)
1505 1507 # hardlink backup is okay because _writedirstate is always called
1506 1508 # with an "atomictemp=True" file.
1507 1509 util.copyfile(
1508 1510 self._opener.join(filename),
1509 1511 self._opener.join(backupname),
1510 1512 hardlink=True,
1511 1513 )
1512 1514
1513 1515 def restorebackup(self, tr, backupname):
1514 1516 '''Restore dirstate by backup file'''
1515 1517 # this "invalidate()" prevents "wlock.release()" from writing
1516 1518 # changes of dirstate out after restoring from backup file
1517 1519 self.invalidate()
1518 1520 filename = self._actualfilename(tr)
1519 1521 o = self._opener
1520 1522 if util.samefile(o.join(backupname), o.join(filename)):
1521 1523 o.unlink(backupname)
1522 1524 else:
1523 1525 o.rename(backupname, filename, checkambig=True)
1524 1526
1525 1527 def clearbackup(self, tr, backupname):
1526 1528 '''Clear backup file'''
1527 1529 self._opener.unlink(backupname)
1528 1530
1529 1531 def verify(self, m1, m2):
1530 1532 """check the dirstate content again the parent manifest and yield errors"""
1531 1533 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1532 1534 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1533 1535 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1534 1536 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1535 1537 for f, entry in self.items():
1536 1538 state = entry.state
1537 1539 if state in b"nr" and f not in m1:
1538 1540 yield (missing_from_p1, f, state)
1539 1541 if state in b"a" and f in m1:
1540 1542 yield (unexpected_in_p1, f, state)
1541 1543 if state in b"m" and f not in m1 and f not in m2:
1542 1544 yield (missing_from_ps, f, state)
1543 1545 for f in m1:
1544 1546 state = self.get_entry(f).state
1545 1547 if state not in b"nrm":
1546 1548 yield (missing_from_ds, f, state)
General Comments 0
You need to be logged in to leave comments. Login now