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