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