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