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