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