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