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