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