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