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