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