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