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