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