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