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