##// END OF EJS Templates
dirstate: replace the use of `_normallookup` in `rebuild`...
marmoute -
r48806:14fa2e58 default
parent child Browse files
Show More
@@ -1,1612 +1,1622
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 559 if self._map.dropfile(filename):
560 560 self._dirty = True
561 561 self._updatedfiles.add(filename)
562 562 elif (not p1_tracked) and wc_tracked:
563 563 if entry is not None and entry.added:
564 564 return # avoid dropping copy information (maybe?)
565 565 elif p1_tracked and not wc_tracked:
566 566 pass
567 567 else:
568 568 assert False, 'unreachable'
569 569
570 570 # this mean we are doing call for file we do not really care about the
571 571 # data (eg: added or removed), however this should be a minor overhead
572 572 # compared to the overall update process calling this.
573 573 parentfiledata = None
574 574 if wc_tracked:
575 575 parentfiledata = self._get_filedata(filename)
576 576
577 577 self._updatedfiles.add(filename)
578 578 self._map.reset_state(
579 579 filename,
580 580 wc_tracked,
581 581 p1_tracked,
582 582 possibly_dirty=possibly_dirty,
583 583 parentfiledata=parentfiledata,
584 584 )
585 585 if (
586 586 parentfiledata is not None
587 587 and parentfiledata[2] > self._lastnormaltime
588 588 ):
589 589 # Remember the most recent modification timeslot for status(),
590 590 # to make sure we won't miss future size-preserving file content
591 591 # modifications that happen within the same timeslot.
592 592 self._lastnormaltime = parentfiledata[2]
593 593
594 594 @requires_parents_change
595 595 def update_file(
596 596 self,
597 597 filename,
598 598 wc_tracked,
599 599 p1_tracked,
600 600 p2_tracked=False,
601 601 merged=False,
602 602 clean_p1=False,
603 603 clean_p2=False,
604 604 possibly_dirty=False,
605 605 parentfiledata=None,
606 606 ):
607 607 """update the information about a file in the dirstate
608 608
609 609 This is to be called when the direstates parent changes to keep track
610 610 of what is the file situation in regards to the working copy and its parent.
611 611
612 612 This function must be called within a `dirstate.parentchange` context.
613 613
614 614 note: the API is at an early stage and we might need to adjust it
615 615 depending of what information ends up being relevant and useful to
616 616 other processing.
617 617 """
618 618 if merged and (clean_p1 or clean_p2):
619 619 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
620 620 raise error.ProgrammingError(msg)
621 621
622 622 # note: I do not think we need to double check name clash here since we
623 623 # are in a update/merge case that should already have taken care of
624 624 # this. The test agrees
625 625
626 626 self._dirty = True
627 627 self._updatedfiles.add(filename)
628 628
629 629 need_parent_file_data = (
630 630 not (possibly_dirty or clean_p2 or merged)
631 631 and wc_tracked
632 632 and p1_tracked
633 633 )
634 634
635 635 # this mean we are doing call for file we do not really care about the
636 636 # data (eg: added or removed), however this should be a minor overhead
637 637 # compared to the overall update process calling this.
638 638 if need_parent_file_data:
639 639 if parentfiledata is None:
640 640 parentfiledata = self._get_filedata(filename)
641 641 mtime = parentfiledata[2]
642 642
643 643 if mtime > self._lastnormaltime:
644 644 # Remember the most recent modification timeslot for
645 645 # status(), to make sure we won't miss future
646 646 # size-preserving file content modifications that happen
647 647 # within the same timeslot.
648 648 self._lastnormaltime = mtime
649 649
650 650 self._map.reset_state(
651 651 filename,
652 652 wc_tracked,
653 653 p1_tracked,
654 654 p2_tracked=p2_tracked,
655 655 merged=merged,
656 656 clean_p1=clean_p1,
657 657 clean_p2=clean_p2,
658 658 possibly_dirty=possibly_dirty,
659 659 parentfiledata=parentfiledata,
660 660 )
661 661 if (
662 662 parentfiledata is not None
663 663 and parentfiledata[2] > self._lastnormaltime
664 664 ):
665 665 # Remember the most recent modification timeslot for status(),
666 666 # to make sure we won't miss future size-preserving file content
667 667 # modifications that happen within the same timeslot.
668 668 self._lastnormaltime = parentfiledata[2]
669 669
670 670 def _addpath(
671 671 self,
672 672 f,
673 673 mode=0,
674 674 size=None,
675 675 mtime=None,
676 676 added=False,
677 677 merged=False,
678 678 from_p2=False,
679 679 possibly_dirty=False,
680 680 ):
681 681 entry = self._map.get(f)
682 682 if added or entry is not None and not entry.tracked:
683 683 self._check_new_tracked_filename(f)
684 684 self._dirty = True
685 685 self._updatedfiles.add(f)
686 686 self._map.addfile(
687 687 f,
688 688 mode=mode,
689 689 size=size,
690 690 mtime=mtime,
691 691 added=added,
692 692 merged=merged,
693 693 from_p2=from_p2,
694 694 possibly_dirty=possibly_dirty,
695 695 )
696 696
697 697 def _check_new_tracked_filename(self, filename):
698 698 scmutil.checkfilename(filename)
699 699 if self._map.hastrackeddir(filename):
700 700 msg = _(b'directory %r already in dirstate')
701 701 msg %= pycompat.bytestr(filename)
702 702 raise error.Abort(msg)
703 703 # shadows
704 704 for d in pathutil.finddirs(filename):
705 705 if self._map.hastrackeddir(d):
706 706 break
707 707 entry = self._map.get(d)
708 708 if entry is not None and not entry.removed:
709 709 msg = _(b'file %r in dirstate clashes with %r')
710 710 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
711 711 raise error.Abort(msg)
712 712
713 713 def _get_filedata(self, filename):
714 714 """returns"""
715 715 s = os.lstat(self._join(filename))
716 716 mode = s.st_mode
717 717 size = s.st_size
718 718 mtime = s[stat.ST_MTIME]
719 719 return (mode, size, mtime)
720 720
721 721 def _normallookup(self, f):
722 722 '''Mark a file normal, but possibly dirty.'''
723 723 if self.in_merge:
724 724 # if there is a merge going on and the file was either
725 725 # "merged" or coming from other parent (-2) before
726 726 # being removed, restore that state.
727 727 entry = self._map.get(f)
728 728 if entry is not None:
729 729 # XXX this should probably be dealt with a a lower level
730 730 # (see `merged_removed` and `from_p2_removed`)
731 731 if entry.merged_removed or entry.from_p2_removed:
732 732 source = self._map.copymap.get(f)
733 733 if entry.merged_removed:
734 734 self._addpath(f, merged=True)
735 735 else:
736 736 self._addpath(f, from_p2=True)
737 737 self._map.copymap.pop(f, None)
738 738 if source is not None:
739 739 self.copy(source, f)
740 740 return
741 741 elif entry.merged or entry.from_p2:
742 742 return
743 743 self._addpath(f, possibly_dirty=True)
744 744 self._map.copymap.pop(f, None)
745 745
746 746 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
747 747 if exists is None:
748 748 exists = os.path.lexists(os.path.join(self._root, path))
749 749 if not exists:
750 750 # Maybe a path component exists
751 751 if not ignoremissing and b'/' in path:
752 752 d, f = path.rsplit(b'/', 1)
753 753 d = self._normalize(d, False, ignoremissing, None)
754 754 folded = d + b"/" + f
755 755 else:
756 756 # No path components, preserve original case
757 757 folded = path
758 758 else:
759 759 # recursively normalize leading directory components
760 760 # against dirstate
761 761 if b'/' in normed:
762 762 d, f = normed.rsplit(b'/', 1)
763 763 d = self._normalize(d, False, ignoremissing, True)
764 764 r = self._root + b"/" + d
765 765 folded = d + b"/" + util.fspath(f, r)
766 766 else:
767 767 folded = util.fspath(normed, self._root)
768 768 storemap[normed] = folded
769 769
770 770 return folded
771 771
772 772 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
773 773 normed = util.normcase(path)
774 774 folded = self._map.filefoldmap.get(normed, None)
775 775 if folded is None:
776 776 if isknown:
777 777 folded = path
778 778 else:
779 779 folded = self._discoverpath(
780 780 path, normed, ignoremissing, exists, self._map.filefoldmap
781 781 )
782 782 return folded
783 783
784 784 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
785 785 normed = util.normcase(path)
786 786 folded = self._map.filefoldmap.get(normed, None)
787 787 if folded is None:
788 788 folded = self._map.dirfoldmap.get(normed, None)
789 789 if folded is None:
790 790 if isknown:
791 791 folded = path
792 792 else:
793 793 # store discovered result in dirfoldmap so that future
794 794 # normalizefile calls don't start matching directories
795 795 folded = self._discoverpath(
796 796 path, normed, ignoremissing, exists, self._map.dirfoldmap
797 797 )
798 798 return folded
799 799
800 800 def normalize(self, path, isknown=False, ignoremissing=False):
801 801 """
802 802 normalize the case of a pathname when on a casefolding filesystem
803 803
804 804 isknown specifies whether the filename came from walking the
805 805 disk, to avoid extra filesystem access.
806 806
807 807 If ignoremissing is True, missing path are returned
808 808 unchanged. Otherwise, we try harder to normalize possibly
809 809 existing path components.
810 810
811 811 The normalized case is determined based on the following precedence:
812 812
813 813 - version of name already stored in the dirstate
814 814 - version of name stored on disk
815 815 - version provided via command arguments
816 816 """
817 817
818 818 if self._checkcase:
819 819 return self._normalize(path, isknown, ignoremissing)
820 820 return path
821 821
822 822 def clear(self):
823 823 self._map.clear()
824 824 self._lastnormaltime = 0
825 825 self._updatedfiles.clear()
826 826 self._dirty = True
827 827
828 828 def rebuild(self, parent, allfiles, changedfiles=None):
829 829 if changedfiles is None:
830 830 # Rebuild entire dirstate
831 831 to_lookup = allfiles
832 832 to_drop = []
833 833 lastnormaltime = self._lastnormaltime
834 834 self.clear()
835 835 self._lastnormaltime = lastnormaltime
836 836 elif len(changedfiles) < 10:
837 837 # Avoid turning allfiles into a set, which can be expensive if it's
838 838 # large.
839 839 to_lookup = []
840 840 to_drop = []
841 841 for f in changedfiles:
842 842 if f in allfiles:
843 843 to_lookup.append(f)
844 844 else:
845 845 to_drop.append(f)
846 846 else:
847 847 changedfilesset = set(changedfiles)
848 848 to_lookup = changedfilesset & set(allfiles)
849 849 to_drop = changedfilesset - to_lookup
850 850
851 851 if self._origpl is None:
852 852 self._origpl = self._pl
853 853 self._map.setparents(parent, self._nodeconstants.nullid)
854 854
855 855 for f in to_lookup:
856 self._normallookup(f)
856
857 if self.in_merge:
858 self.set_tracked(f)
859 else:
860 self._map.reset_state(
861 f,
862 wc_tracked=True,
863 p1_tracked=True,
864 possibly_dirty=True,
865 )
866 self._updatedfiles.add(f)
857 867 for f in to_drop:
858 868 if self._map.dropfile(f):
859 869 self._updatedfiles.add(f)
860 870
861 871 self._dirty = True
862 872
863 873 def identity(self):
864 874 """Return identity of dirstate itself to detect changing in storage
865 875
866 876 If identity of previous dirstate is equal to this, writing
867 877 changes based on the former dirstate out can keep consistency.
868 878 """
869 879 return self._map.identity
870 880
871 881 def write(self, tr):
872 882 if not self._dirty:
873 883 return
874 884
875 885 filename = self._filename
876 886 if tr:
877 887 # 'dirstate.write()' is not only for writing in-memory
878 888 # changes out, but also for dropping ambiguous timestamp.
879 889 # delayed writing re-raise "ambiguous timestamp issue".
880 890 # See also the wiki page below for detail:
881 891 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
882 892
883 893 # emulate dropping timestamp in 'parsers.pack_dirstate'
884 894 now = _getfsnow(self._opener)
885 895 self._map.clearambiguoustimes(self._updatedfiles, now)
886 896
887 897 # emulate that all 'dirstate.normal' results are written out
888 898 self._lastnormaltime = 0
889 899 self._updatedfiles.clear()
890 900
891 901 # delay writing in-memory changes out
892 902 tr.addfilegenerator(
893 903 b'dirstate',
894 904 (self._filename,),
895 905 lambda f: self._writedirstate(tr, f),
896 906 location=b'plain',
897 907 )
898 908 return
899 909
900 910 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
901 911 self._writedirstate(tr, st)
902 912
903 913 def addparentchangecallback(self, category, callback):
904 914 """add a callback to be called when the wd parents are changed
905 915
906 916 Callback will be called with the following arguments:
907 917 dirstate, (oldp1, oldp2), (newp1, newp2)
908 918
909 919 Category is a unique identifier to allow overwriting an old callback
910 920 with a newer callback.
911 921 """
912 922 self._plchangecallbacks[category] = callback
913 923
914 924 def _writedirstate(self, tr, st):
915 925 # notify callbacks about parents change
916 926 if self._origpl is not None and self._origpl != self._pl:
917 927 for c, callback in sorted(
918 928 pycompat.iteritems(self._plchangecallbacks)
919 929 ):
920 930 callback(self, self._origpl, self._pl)
921 931 self._origpl = None
922 932 # use the modification time of the newly created temporary file as the
923 933 # filesystem's notion of 'now'
924 934 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
925 935
926 936 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
927 937 # timestamp of each entries in dirstate, because of 'now > mtime'
928 938 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
929 939 if delaywrite > 0:
930 940 # do we have any files to delay for?
931 941 for f, e in pycompat.iteritems(self._map):
932 942 if e.need_delay(now):
933 943 import time # to avoid useless import
934 944
935 945 # rather than sleep n seconds, sleep until the next
936 946 # multiple of n seconds
937 947 clock = time.time()
938 948 start = int(clock) - (int(clock) % delaywrite)
939 949 end = start + delaywrite
940 950 time.sleep(end - clock)
941 951 now = end # trust our estimate that the end is near now
942 952 break
943 953
944 954 self._map.write(tr, st, now)
945 955 self._lastnormaltime = 0
946 956 self._dirty = False
947 957
948 958 def _dirignore(self, f):
949 959 if self._ignore(f):
950 960 return True
951 961 for p in pathutil.finddirs(f):
952 962 if self._ignore(p):
953 963 return True
954 964 return False
955 965
956 966 def _ignorefiles(self):
957 967 files = []
958 968 if os.path.exists(self._join(b'.hgignore')):
959 969 files.append(self._join(b'.hgignore'))
960 970 for name, path in self._ui.configitems(b"ui"):
961 971 if name == b'ignore' or name.startswith(b'ignore.'):
962 972 # we need to use os.path.join here rather than self._join
963 973 # because path is arbitrary and user-specified
964 974 files.append(os.path.join(self._rootdir, util.expandpath(path)))
965 975 return files
966 976
967 977 def _ignorefileandline(self, f):
968 978 files = collections.deque(self._ignorefiles())
969 979 visited = set()
970 980 while files:
971 981 i = files.popleft()
972 982 patterns = matchmod.readpatternfile(
973 983 i, self._ui.warn, sourceinfo=True
974 984 )
975 985 for pattern, lineno, line in patterns:
976 986 kind, p = matchmod._patsplit(pattern, b'glob')
977 987 if kind == b"subinclude":
978 988 if p not in visited:
979 989 files.append(p)
980 990 continue
981 991 m = matchmod.match(
982 992 self._root, b'', [], [pattern], warn=self._ui.warn
983 993 )
984 994 if m(f):
985 995 return (i, lineno, line)
986 996 visited.add(i)
987 997 return (None, -1, b"")
988 998
989 999 def _walkexplicit(self, match, subrepos):
990 1000 """Get stat data about the files explicitly specified by match.
991 1001
992 1002 Return a triple (results, dirsfound, dirsnotfound).
993 1003 - results is a mapping from filename to stat result. It also contains
994 1004 listings mapping subrepos and .hg to None.
995 1005 - dirsfound is a list of files found to be directories.
996 1006 - dirsnotfound is a list of files that the dirstate thinks are
997 1007 directories and that were not found."""
998 1008
999 1009 def badtype(mode):
1000 1010 kind = _(b'unknown')
1001 1011 if stat.S_ISCHR(mode):
1002 1012 kind = _(b'character device')
1003 1013 elif stat.S_ISBLK(mode):
1004 1014 kind = _(b'block device')
1005 1015 elif stat.S_ISFIFO(mode):
1006 1016 kind = _(b'fifo')
1007 1017 elif stat.S_ISSOCK(mode):
1008 1018 kind = _(b'socket')
1009 1019 elif stat.S_ISDIR(mode):
1010 1020 kind = _(b'directory')
1011 1021 return _(b'unsupported file type (type is %s)') % kind
1012 1022
1013 1023 badfn = match.bad
1014 1024 dmap = self._map
1015 1025 lstat = os.lstat
1016 1026 getkind = stat.S_IFMT
1017 1027 dirkind = stat.S_IFDIR
1018 1028 regkind = stat.S_IFREG
1019 1029 lnkkind = stat.S_IFLNK
1020 1030 join = self._join
1021 1031 dirsfound = []
1022 1032 foundadd = dirsfound.append
1023 1033 dirsnotfound = []
1024 1034 notfoundadd = dirsnotfound.append
1025 1035
1026 1036 if not match.isexact() and self._checkcase:
1027 1037 normalize = self._normalize
1028 1038 else:
1029 1039 normalize = None
1030 1040
1031 1041 files = sorted(match.files())
1032 1042 subrepos.sort()
1033 1043 i, j = 0, 0
1034 1044 while i < len(files) and j < len(subrepos):
1035 1045 subpath = subrepos[j] + b"/"
1036 1046 if files[i] < subpath:
1037 1047 i += 1
1038 1048 continue
1039 1049 while i < len(files) and files[i].startswith(subpath):
1040 1050 del files[i]
1041 1051 j += 1
1042 1052
1043 1053 if not files or b'' in files:
1044 1054 files = [b'']
1045 1055 # constructing the foldmap is expensive, so don't do it for the
1046 1056 # common case where files is ['']
1047 1057 normalize = None
1048 1058 results = dict.fromkeys(subrepos)
1049 1059 results[b'.hg'] = None
1050 1060
1051 1061 for ff in files:
1052 1062 if normalize:
1053 1063 nf = normalize(ff, False, True)
1054 1064 else:
1055 1065 nf = ff
1056 1066 if nf in results:
1057 1067 continue
1058 1068
1059 1069 try:
1060 1070 st = lstat(join(nf))
1061 1071 kind = getkind(st.st_mode)
1062 1072 if kind == dirkind:
1063 1073 if nf in dmap:
1064 1074 # file replaced by dir on disk but still in dirstate
1065 1075 results[nf] = None
1066 1076 foundadd((nf, ff))
1067 1077 elif kind == regkind or kind == lnkkind:
1068 1078 results[nf] = st
1069 1079 else:
1070 1080 badfn(ff, badtype(kind))
1071 1081 if nf in dmap:
1072 1082 results[nf] = None
1073 1083 except OSError as inst: # nf not found on disk - it is dirstate only
1074 1084 if nf in dmap: # does it exactly match a missing file?
1075 1085 results[nf] = None
1076 1086 else: # does it match a missing directory?
1077 1087 if self._map.hasdir(nf):
1078 1088 notfoundadd(nf)
1079 1089 else:
1080 1090 badfn(ff, encoding.strtolocal(inst.strerror))
1081 1091
1082 1092 # match.files() may contain explicitly-specified paths that shouldn't
1083 1093 # be taken; drop them from the list of files found. dirsfound/notfound
1084 1094 # aren't filtered here because they will be tested later.
1085 1095 if match.anypats():
1086 1096 for f in list(results):
1087 1097 if f == b'.hg' or f in subrepos:
1088 1098 # keep sentinel to disable further out-of-repo walks
1089 1099 continue
1090 1100 if not match(f):
1091 1101 del results[f]
1092 1102
1093 1103 # Case insensitive filesystems cannot rely on lstat() failing to detect
1094 1104 # a case-only rename. Prune the stat object for any file that does not
1095 1105 # match the case in the filesystem, if there are multiple files that
1096 1106 # normalize to the same path.
1097 1107 if match.isexact() and self._checkcase:
1098 1108 normed = {}
1099 1109
1100 1110 for f, st in pycompat.iteritems(results):
1101 1111 if st is None:
1102 1112 continue
1103 1113
1104 1114 nc = util.normcase(f)
1105 1115 paths = normed.get(nc)
1106 1116
1107 1117 if paths is None:
1108 1118 paths = set()
1109 1119 normed[nc] = paths
1110 1120
1111 1121 paths.add(f)
1112 1122
1113 1123 for norm, paths in pycompat.iteritems(normed):
1114 1124 if len(paths) > 1:
1115 1125 for path in paths:
1116 1126 folded = self._discoverpath(
1117 1127 path, norm, True, None, self._map.dirfoldmap
1118 1128 )
1119 1129 if path != folded:
1120 1130 results[path] = None
1121 1131
1122 1132 return results, dirsfound, dirsnotfound
1123 1133
1124 1134 def walk(self, match, subrepos, unknown, ignored, full=True):
1125 1135 """
1126 1136 Walk recursively through the directory tree, finding all files
1127 1137 matched by match.
1128 1138
1129 1139 If full is False, maybe skip some known-clean files.
1130 1140
1131 1141 Return a dict mapping filename to stat-like object (either
1132 1142 mercurial.osutil.stat instance or return value of os.stat()).
1133 1143
1134 1144 """
1135 1145 # full is a flag that extensions that hook into walk can use -- this
1136 1146 # implementation doesn't use it at all. This satisfies the contract
1137 1147 # because we only guarantee a "maybe".
1138 1148
1139 1149 if ignored:
1140 1150 ignore = util.never
1141 1151 dirignore = util.never
1142 1152 elif unknown:
1143 1153 ignore = self._ignore
1144 1154 dirignore = self._dirignore
1145 1155 else:
1146 1156 # if not unknown and not ignored, drop dir recursion and step 2
1147 1157 ignore = util.always
1148 1158 dirignore = util.always
1149 1159
1150 1160 matchfn = match.matchfn
1151 1161 matchalways = match.always()
1152 1162 matchtdir = match.traversedir
1153 1163 dmap = self._map
1154 1164 listdir = util.listdir
1155 1165 lstat = os.lstat
1156 1166 dirkind = stat.S_IFDIR
1157 1167 regkind = stat.S_IFREG
1158 1168 lnkkind = stat.S_IFLNK
1159 1169 join = self._join
1160 1170
1161 1171 exact = skipstep3 = False
1162 1172 if match.isexact(): # match.exact
1163 1173 exact = True
1164 1174 dirignore = util.always # skip step 2
1165 1175 elif match.prefix(): # match.match, no patterns
1166 1176 skipstep3 = True
1167 1177
1168 1178 if not exact and self._checkcase:
1169 1179 normalize = self._normalize
1170 1180 normalizefile = self._normalizefile
1171 1181 skipstep3 = False
1172 1182 else:
1173 1183 normalize = self._normalize
1174 1184 normalizefile = None
1175 1185
1176 1186 # step 1: find all explicit files
1177 1187 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1178 1188 if matchtdir:
1179 1189 for d in work:
1180 1190 matchtdir(d[0])
1181 1191 for d in dirsnotfound:
1182 1192 matchtdir(d)
1183 1193
1184 1194 skipstep3 = skipstep3 and not (work or dirsnotfound)
1185 1195 work = [d for d in work if not dirignore(d[0])]
1186 1196
1187 1197 # step 2: visit subdirectories
1188 1198 def traverse(work, alreadynormed):
1189 1199 wadd = work.append
1190 1200 while work:
1191 1201 tracing.counter('dirstate.walk work', len(work))
1192 1202 nd = work.pop()
1193 1203 visitentries = match.visitchildrenset(nd)
1194 1204 if not visitentries:
1195 1205 continue
1196 1206 if visitentries == b'this' or visitentries == b'all':
1197 1207 visitentries = None
1198 1208 skip = None
1199 1209 if nd != b'':
1200 1210 skip = b'.hg'
1201 1211 try:
1202 1212 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1203 1213 entries = listdir(join(nd), stat=True, skip=skip)
1204 1214 except OSError as inst:
1205 1215 if inst.errno in (errno.EACCES, errno.ENOENT):
1206 1216 match.bad(
1207 1217 self.pathto(nd), encoding.strtolocal(inst.strerror)
1208 1218 )
1209 1219 continue
1210 1220 raise
1211 1221 for f, kind, st in entries:
1212 1222 # Some matchers may return files in the visitentries set,
1213 1223 # instead of 'this', if the matcher explicitly mentions them
1214 1224 # and is not an exactmatcher. This is acceptable; we do not
1215 1225 # make any hard assumptions about file-or-directory below
1216 1226 # based on the presence of `f` in visitentries. If
1217 1227 # visitchildrenset returned a set, we can always skip the
1218 1228 # entries *not* in the set it provided regardless of whether
1219 1229 # they're actually a file or a directory.
1220 1230 if visitentries and f not in visitentries:
1221 1231 continue
1222 1232 if normalizefile:
1223 1233 # even though f might be a directory, we're only
1224 1234 # interested in comparing it to files currently in the
1225 1235 # dmap -- therefore normalizefile is enough
1226 1236 nf = normalizefile(
1227 1237 nd and (nd + b"/" + f) or f, True, True
1228 1238 )
1229 1239 else:
1230 1240 nf = nd and (nd + b"/" + f) or f
1231 1241 if nf not in results:
1232 1242 if kind == dirkind:
1233 1243 if not ignore(nf):
1234 1244 if matchtdir:
1235 1245 matchtdir(nf)
1236 1246 wadd(nf)
1237 1247 if nf in dmap and (matchalways or matchfn(nf)):
1238 1248 results[nf] = None
1239 1249 elif kind == regkind or kind == lnkkind:
1240 1250 if nf in dmap:
1241 1251 if matchalways or matchfn(nf):
1242 1252 results[nf] = st
1243 1253 elif (matchalways or matchfn(nf)) and not ignore(
1244 1254 nf
1245 1255 ):
1246 1256 # unknown file -- normalize if necessary
1247 1257 if not alreadynormed:
1248 1258 nf = normalize(nf, False, True)
1249 1259 results[nf] = st
1250 1260 elif nf in dmap and (matchalways or matchfn(nf)):
1251 1261 results[nf] = None
1252 1262
1253 1263 for nd, d in work:
1254 1264 # alreadynormed means that processwork doesn't have to do any
1255 1265 # expensive directory normalization
1256 1266 alreadynormed = not normalize or nd == d
1257 1267 traverse([d], alreadynormed)
1258 1268
1259 1269 for s in subrepos:
1260 1270 del results[s]
1261 1271 del results[b'.hg']
1262 1272
1263 1273 # step 3: visit remaining files from dmap
1264 1274 if not skipstep3 and not exact:
1265 1275 # If a dmap file is not in results yet, it was either
1266 1276 # a) not matching matchfn b) ignored, c) missing, or d) under a
1267 1277 # symlink directory.
1268 1278 if not results and matchalways:
1269 1279 visit = [f for f in dmap]
1270 1280 else:
1271 1281 visit = [f for f in dmap if f not in results and matchfn(f)]
1272 1282 visit.sort()
1273 1283
1274 1284 if unknown:
1275 1285 # unknown == True means we walked all dirs under the roots
1276 1286 # that wasn't ignored, and everything that matched was stat'ed
1277 1287 # and is already in results.
1278 1288 # The rest must thus be ignored or under a symlink.
1279 1289 audit_path = pathutil.pathauditor(self._root, cached=True)
1280 1290
1281 1291 for nf in iter(visit):
1282 1292 # If a stat for the same file was already added with a
1283 1293 # different case, don't add one for this, since that would
1284 1294 # make it appear as if the file exists under both names
1285 1295 # on disk.
1286 1296 if (
1287 1297 normalizefile
1288 1298 and normalizefile(nf, True, True) in results
1289 1299 ):
1290 1300 results[nf] = None
1291 1301 # Report ignored items in the dmap as long as they are not
1292 1302 # under a symlink directory.
1293 1303 elif audit_path.check(nf):
1294 1304 try:
1295 1305 results[nf] = lstat(join(nf))
1296 1306 # file was just ignored, no links, and exists
1297 1307 except OSError:
1298 1308 # file doesn't exist
1299 1309 results[nf] = None
1300 1310 else:
1301 1311 # It's either missing or under a symlink directory
1302 1312 # which we in this case report as missing
1303 1313 results[nf] = None
1304 1314 else:
1305 1315 # We may not have walked the full directory tree above,
1306 1316 # so stat and check everything we missed.
1307 1317 iv = iter(visit)
1308 1318 for st in util.statfiles([join(i) for i in visit]):
1309 1319 results[next(iv)] = st
1310 1320 return results
1311 1321
1312 1322 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1313 1323 # Force Rayon (Rust parallelism library) to respect the number of
1314 1324 # workers. This is a temporary workaround until Rust code knows
1315 1325 # how to read the config file.
1316 1326 numcpus = self._ui.configint(b"worker", b"numcpus")
1317 1327 if numcpus is not None:
1318 1328 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1319 1329
1320 1330 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1321 1331 if not workers_enabled:
1322 1332 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1323 1333
1324 1334 (
1325 1335 lookup,
1326 1336 modified,
1327 1337 added,
1328 1338 removed,
1329 1339 deleted,
1330 1340 clean,
1331 1341 ignored,
1332 1342 unknown,
1333 1343 warnings,
1334 1344 bad,
1335 1345 traversed,
1336 1346 dirty,
1337 1347 ) = rustmod.status(
1338 1348 self._map._rustmap,
1339 1349 matcher,
1340 1350 self._rootdir,
1341 1351 self._ignorefiles(),
1342 1352 self._checkexec,
1343 1353 self._lastnormaltime,
1344 1354 bool(list_clean),
1345 1355 bool(list_ignored),
1346 1356 bool(list_unknown),
1347 1357 bool(matcher.traversedir),
1348 1358 )
1349 1359
1350 1360 self._dirty |= dirty
1351 1361
1352 1362 if matcher.traversedir:
1353 1363 for dir in traversed:
1354 1364 matcher.traversedir(dir)
1355 1365
1356 1366 if self._ui.warn:
1357 1367 for item in warnings:
1358 1368 if isinstance(item, tuple):
1359 1369 file_path, syntax = item
1360 1370 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1361 1371 file_path,
1362 1372 syntax,
1363 1373 )
1364 1374 self._ui.warn(msg)
1365 1375 else:
1366 1376 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1367 1377 self._ui.warn(
1368 1378 msg
1369 1379 % (
1370 1380 pathutil.canonpath(
1371 1381 self._rootdir, self._rootdir, item
1372 1382 ),
1373 1383 b"No such file or directory",
1374 1384 )
1375 1385 )
1376 1386
1377 1387 for (fn, message) in bad:
1378 1388 matcher.bad(fn, encoding.strtolocal(message))
1379 1389
1380 1390 status = scmutil.status(
1381 1391 modified=modified,
1382 1392 added=added,
1383 1393 removed=removed,
1384 1394 deleted=deleted,
1385 1395 unknown=unknown,
1386 1396 ignored=ignored,
1387 1397 clean=clean,
1388 1398 )
1389 1399 return (lookup, status)
1390 1400
1391 1401 def status(self, match, subrepos, ignored, clean, unknown):
1392 1402 """Determine the status of the working copy relative to the
1393 1403 dirstate and return a pair of (unsure, status), where status is of type
1394 1404 scmutil.status and:
1395 1405
1396 1406 unsure:
1397 1407 files that might have been modified since the dirstate was
1398 1408 written, but need to be read to be sure (size is the same
1399 1409 but mtime differs)
1400 1410 status.modified:
1401 1411 files that have definitely been modified since the dirstate
1402 1412 was written (different size or mode)
1403 1413 status.clean:
1404 1414 files that have definitely not been modified since the
1405 1415 dirstate was written
1406 1416 """
1407 1417 listignored, listclean, listunknown = ignored, clean, unknown
1408 1418 lookup, modified, added, unknown, ignored = [], [], [], [], []
1409 1419 removed, deleted, clean = [], [], []
1410 1420
1411 1421 dmap = self._map
1412 1422 dmap.preload()
1413 1423
1414 1424 use_rust = True
1415 1425
1416 1426 allowed_matchers = (
1417 1427 matchmod.alwaysmatcher,
1418 1428 matchmod.exactmatcher,
1419 1429 matchmod.includematcher,
1420 1430 )
1421 1431
1422 1432 if rustmod is None:
1423 1433 use_rust = False
1424 1434 elif self._checkcase:
1425 1435 # Case-insensitive filesystems are not handled yet
1426 1436 use_rust = False
1427 1437 elif subrepos:
1428 1438 use_rust = False
1429 1439 elif sparse.enabled:
1430 1440 use_rust = False
1431 1441 elif not isinstance(match, allowed_matchers):
1432 1442 # Some matchers have yet to be implemented
1433 1443 use_rust = False
1434 1444
1435 1445 if use_rust:
1436 1446 try:
1437 1447 return self._rust_status(
1438 1448 match, listclean, listignored, listunknown
1439 1449 )
1440 1450 except rustmod.FallbackError:
1441 1451 pass
1442 1452
1443 1453 def noop(f):
1444 1454 pass
1445 1455
1446 1456 dcontains = dmap.__contains__
1447 1457 dget = dmap.__getitem__
1448 1458 ladd = lookup.append # aka "unsure"
1449 1459 madd = modified.append
1450 1460 aadd = added.append
1451 1461 uadd = unknown.append if listunknown else noop
1452 1462 iadd = ignored.append if listignored else noop
1453 1463 radd = removed.append
1454 1464 dadd = deleted.append
1455 1465 cadd = clean.append if listclean else noop
1456 1466 mexact = match.exact
1457 1467 dirignore = self._dirignore
1458 1468 checkexec = self._checkexec
1459 1469 copymap = self._map.copymap
1460 1470 lastnormaltime = self._lastnormaltime
1461 1471
1462 1472 # We need to do full walks when either
1463 1473 # - we're listing all clean files, or
1464 1474 # - match.traversedir does something, because match.traversedir should
1465 1475 # be called for every dir in the working dir
1466 1476 full = listclean or match.traversedir is not None
1467 1477 for fn, st in pycompat.iteritems(
1468 1478 self.walk(match, subrepos, listunknown, listignored, full=full)
1469 1479 ):
1470 1480 if not dcontains(fn):
1471 1481 if (listignored or mexact(fn)) and dirignore(fn):
1472 1482 if listignored:
1473 1483 iadd(fn)
1474 1484 else:
1475 1485 uadd(fn)
1476 1486 continue
1477 1487
1478 1488 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1479 1489 # written like that for performance reasons. dmap[fn] is not a
1480 1490 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1481 1491 # opcode has fast paths when the value to be unpacked is a tuple or
1482 1492 # a list, but falls back to creating a full-fledged iterator in
1483 1493 # general. That is much slower than simply accessing and storing the
1484 1494 # tuple members one by one.
1485 1495 t = dget(fn)
1486 1496 mode = t.mode
1487 1497 size = t.size
1488 1498 time = t.mtime
1489 1499
1490 1500 if not st and t.tracked:
1491 1501 dadd(fn)
1492 1502 elif t.merged:
1493 1503 madd(fn)
1494 1504 elif t.added:
1495 1505 aadd(fn)
1496 1506 elif t.removed:
1497 1507 radd(fn)
1498 1508 elif t.tracked:
1499 1509 if (
1500 1510 size >= 0
1501 1511 and (
1502 1512 (size != st.st_size and size != st.st_size & _rangemask)
1503 1513 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1504 1514 )
1505 1515 or t.from_p2
1506 1516 or fn in copymap
1507 1517 ):
1508 1518 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1509 1519 # issue6456: Size returned may be longer due to
1510 1520 # encryption on EXT-4 fscrypt, undecided.
1511 1521 ladd(fn)
1512 1522 else:
1513 1523 madd(fn)
1514 1524 elif (
1515 1525 time != st[stat.ST_MTIME]
1516 1526 and time != st[stat.ST_MTIME] & _rangemask
1517 1527 ):
1518 1528 ladd(fn)
1519 1529 elif st[stat.ST_MTIME] == lastnormaltime:
1520 1530 # fn may have just been marked as normal and it may have
1521 1531 # changed in the same second without changing its size.
1522 1532 # This can happen if we quickly do multiple commits.
1523 1533 # Force lookup, so we don't miss such a racy file change.
1524 1534 ladd(fn)
1525 1535 elif listclean:
1526 1536 cadd(fn)
1527 1537 status = scmutil.status(
1528 1538 modified, added, removed, deleted, unknown, ignored, clean
1529 1539 )
1530 1540 return (lookup, status)
1531 1541
1532 1542 def matches(self, match):
1533 1543 """
1534 1544 return files in the dirstate (in whatever state) filtered by match
1535 1545 """
1536 1546 dmap = self._map
1537 1547 if rustmod is not None:
1538 1548 dmap = self._map._rustmap
1539 1549
1540 1550 if match.always():
1541 1551 return dmap.keys()
1542 1552 files = match.files()
1543 1553 if match.isexact():
1544 1554 # fast path -- filter the other way around, since typically files is
1545 1555 # much smaller than dmap
1546 1556 return [f for f in files if f in dmap]
1547 1557 if match.prefix() and all(fn in dmap for fn in files):
1548 1558 # fast path -- all the values are known to be files, so just return
1549 1559 # that
1550 1560 return list(files)
1551 1561 return [f for f in dmap if match(f)]
1552 1562
1553 1563 def _actualfilename(self, tr):
1554 1564 if tr:
1555 1565 return self._pendingfilename
1556 1566 else:
1557 1567 return self._filename
1558 1568
1559 1569 def savebackup(self, tr, backupname):
1560 1570 '''Save current dirstate into backup file'''
1561 1571 filename = self._actualfilename(tr)
1562 1572 assert backupname != filename
1563 1573
1564 1574 # use '_writedirstate' instead of 'write' to write changes certainly,
1565 1575 # because the latter omits writing out if transaction is running.
1566 1576 # output file will be used to create backup of dirstate at this point.
1567 1577 if self._dirty or not self._opener.exists(filename):
1568 1578 self._writedirstate(
1569 1579 tr,
1570 1580 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1571 1581 )
1572 1582
1573 1583 if tr:
1574 1584 # ensure that subsequent tr.writepending returns True for
1575 1585 # changes written out above, even if dirstate is never
1576 1586 # changed after this
1577 1587 tr.addfilegenerator(
1578 1588 b'dirstate',
1579 1589 (self._filename,),
1580 1590 lambda f: self._writedirstate(tr, f),
1581 1591 location=b'plain',
1582 1592 )
1583 1593
1584 1594 # ensure that pending file written above is unlinked at
1585 1595 # failure, even if tr.writepending isn't invoked until the
1586 1596 # end of this transaction
1587 1597 tr.registertmp(filename, location=b'plain')
1588 1598
1589 1599 self._opener.tryunlink(backupname)
1590 1600 # hardlink backup is okay because _writedirstate is always called
1591 1601 # with an "atomictemp=True" file.
1592 1602 util.copyfile(
1593 1603 self._opener.join(filename),
1594 1604 self._opener.join(backupname),
1595 1605 hardlink=True,
1596 1606 )
1597 1607
1598 1608 def restorebackup(self, tr, backupname):
1599 1609 '''Restore dirstate by backup file'''
1600 1610 # this "invalidate()" prevents "wlock.release()" from writing
1601 1611 # changes of dirstate out after restoring from backup file
1602 1612 self.invalidate()
1603 1613 filename = self._actualfilename(tr)
1604 1614 o = self._opener
1605 1615 if util.samefile(o.join(backupname), o.join(filename)):
1606 1616 o.unlink(backupname)
1607 1617 else:
1608 1618 o.rename(backupname, filename, checkambig=True)
1609 1619
1610 1620 def clearbackup(self, tr, backupname):
1611 1621 '''Clear backup file'''
1612 1622 self._opener.unlink(backupname)
General Comments 0
You need to be logged in to leave comments. Login now