##// END OF EJS Templates
dirstate: do not write an empty dirstate just for backup...
marmoute -
r50897:e358f6e0 default
parent child Browse files
Show More
@@ -1,1613 +1,1629 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 597
598 598 # note: I do not think we need to double check name clash here since we
599 599 # are in a update/merge case that should already have taken care of
600 600 # this. The test agrees
601 601
602 602 self._dirty = True
603 603 old_entry = self._map.get(filename)
604 604 if old_entry is None:
605 605 prev_tracked = False
606 606 else:
607 607 prev_tracked = old_entry.tracked
608 608 if prev_tracked != wc_tracked:
609 609 self._dirty_tracked_set = True
610 610
611 611 self._map.reset_state(
612 612 filename,
613 613 wc_tracked,
614 614 p1_tracked,
615 615 p2_info=p2_info,
616 616 has_meaningful_mtime=not possibly_dirty,
617 617 parentfiledata=parentfiledata,
618 618 )
619 619
620 620 def _check_new_tracked_filename(self, filename):
621 621 scmutil.checkfilename(filename)
622 622 if self._map.hastrackeddir(filename):
623 623 msg = _(b'directory %r already in dirstate')
624 624 msg %= pycompat.bytestr(filename)
625 625 raise error.Abort(msg)
626 626 # shadows
627 627 for d in pathutil.finddirs(filename):
628 628 if self._map.hastrackeddir(d):
629 629 break
630 630 entry = self._map.get(d)
631 631 if entry is not None and not entry.removed:
632 632 msg = _(b'file %r in dirstate clashes with %r')
633 633 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
634 634 raise error.Abort(msg)
635 635 self._check_sparse(filename)
636 636
637 637 def _check_sparse(self, filename):
638 638 """Check that a filename is inside the sparse profile"""
639 639 sparsematch = self._sparsematcher
640 640 if sparsematch is not None and not sparsematch.always():
641 641 if not sparsematch(filename):
642 642 msg = _(b"cannot add '%s' - it is outside the sparse checkout")
643 643 hint = _(
644 644 b'include file with `hg debugsparse --include <pattern>` or use '
645 645 b'`hg add -s <file>` to include file directory while adding'
646 646 )
647 647 raise error.Abort(msg % filename, hint=hint)
648 648
649 649 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
650 650 if exists is None:
651 651 exists = os.path.lexists(os.path.join(self._root, path))
652 652 if not exists:
653 653 # Maybe a path component exists
654 654 if not ignoremissing and b'/' in path:
655 655 d, f = path.rsplit(b'/', 1)
656 656 d = self._normalize(d, False, ignoremissing, None)
657 657 folded = d + b"/" + f
658 658 else:
659 659 # No path components, preserve original case
660 660 folded = path
661 661 else:
662 662 # recursively normalize leading directory components
663 663 # against dirstate
664 664 if b'/' in normed:
665 665 d, f = normed.rsplit(b'/', 1)
666 666 d = self._normalize(d, False, ignoremissing, True)
667 667 r = self._root + b"/" + d
668 668 folded = d + b"/" + util.fspath(f, r)
669 669 else:
670 670 folded = util.fspath(normed, self._root)
671 671 storemap[normed] = folded
672 672
673 673 return folded
674 674
675 675 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
676 676 normed = util.normcase(path)
677 677 folded = self._map.filefoldmap.get(normed, None)
678 678 if folded is None:
679 679 if isknown:
680 680 folded = path
681 681 else:
682 682 folded = self._discoverpath(
683 683 path, normed, ignoremissing, exists, self._map.filefoldmap
684 684 )
685 685 return folded
686 686
687 687 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
688 688 normed = util.normcase(path)
689 689 folded = self._map.filefoldmap.get(normed, None)
690 690 if folded is None:
691 691 folded = self._map.dirfoldmap.get(normed, None)
692 692 if folded is None:
693 693 if isknown:
694 694 folded = path
695 695 else:
696 696 # store discovered result in dirfoldmap so that future
697 697 # normalizefile calls don't start matching directories
698 698 folded = self._discoverpath(
699 699 path, normed, ignoremissing, exists, self._map.dirfoldmap
700 700 )
701 701 return folded
702 702
703 703 def normalize(self, path, isknown=False, ignoremissing=False):
704 704 """
705 705 normalize the case of a pathname when on a casefolding filesystem
706 706
707 707 isknown specifies whether the filename came from walking the
708 708 disk, to avoid extra filesystem access.
709 709
710 710 If ignoremissing is True, missing path are returned
711 711 unchanged. Otherwise, we try harder to normalize possibly
712 712 existing path components.
713 713
714 714 The normalized case is determined based on the following precedence:
715 715
716 716 - version of name already stored in the dirstate
717 717 - version of name stored on disk
718 718 - version provided via command arguments
719 719 """
720 720
721 721 if self._checkcase:
722 722 return self._normalize(path, isknown, ignoremissing)
723 723 return path
724 724
725 725 def clear(self):
726 726 self._map.clear()
727 727 self._dirty = True
728 728
729 729 def rebuild(self, parent, allfiles, changedfiles=None):
730 730
731 731 matcher = self._sparsematcher
732 732 if matcher is not None and not matcher.always():
733 733 # should not add non-matching files
734 734 allfiles = [f for f in allfiles if matcher(f)]
735 735 if changedfiles:
736 736 changedfiles = [f for f in changedfiles if matcher(f)]
737 737
738 738 if changedfiles is not None:
739 739 # these files will be deleted from the dirstate when they are
740 740 # not found to be in allfiles
741 741 dirstatefilestoremove = {f for f in self if not matcher(f)}
742 742 changedfiles = dirstatefilestoremove.union(changedfiles)
743 743
744 744 if changedfiles is None:
745 745 # Rebuild entire dirstate
746 746 to_lookup = allfiles
747 747 to_drop = []
748 748 self.clear()
749 749 elif len(changedfiles) < 10:
750 750 # Avoid turning allfiles into a set, which can be expensive if it's
751 751 # large.
752 752 to_lookup = []
753 753 to_drop = []
754 754 for f in changedfiles:
755 755 if f in allfiles:
756 756 to_lookup.append(f)
757 757 else:
758 758 to_drop.append(f)
759 759 else:
760 760 changedfilesset = set(changedfiles)
761 761 to_lookup = changedfilesset & set(allfiles)
762 762 to_drop = changedfilesset - to_lookup
763 763
764 764 if self._origpl is None:
765 765 self._origpl = self._pl
766 766 self._map.setparents(parent, self._nodeconstants.nullid)
767 767
768 768 for f in to_lookup:
769 769
770 770 if self.in_merge:
771 771 self.set_tracked(f)
772 772 else:
773 773 self._map.reset_state(
774 774 f,
775 775 wc_tracked=True,
776 776 p1_tracked=True,
777 777 )
778 778 for f in to_drop:
779 779 self._map.reset_state(f)
780 780
781 781 self._dirty = True
782 782
783 783 def identity(self):
784 784 """Return identity of dirstate itself to detect changing in storage
785 785
786 786 If identity of previous dirstate is equal to this, writing
787 787 changes based on the former dirstate out can keep consistency.
788 788 """
789 789 return self._map.identity
790 790
791 791 def write(self, tr):
792 792 if not self._dirty:
793 793 return
794 794
795 795 write_key = self._use_tracked_hint and self._dirty_tracked_set
796 796 if tr:
797 797 # delay writing in-memory changes out
798 798 tr.addfilegenerator(
799 799 b'dirstate-1-main',
800 800 (self._filename,),
801 801 lambda f: self._writedirstate(tr, f),
802 802 location=b'plain',
803 803 post_finalize=True,
804 804 )
805 805 if write_key:
806 806 tr.addfilegenerator(
807 807 b'dirstate-2-key-post',
808 808 (self._filename_th,),
809 809 lambda f: self._write_tracked_hint(tr, f),
810 810 location=b'plain',
811 811 post_finalize=True,
812 812 )
813 813 return
814 814
815 815 file = lambda f: self._opener(f, b"w", atomictemp=True, checkambig=True)
816 816 with file(self._filename) as f:
817 817 self._writedirstate(tr, f)
818 818 if write_key:
819 819 # we update the key-file after writing to make sure reader have a
820 820 # key that match the newly written content
821 821 with file(self._filename_th) as f:
822 822 self._write_tracked_hint(tr, f)
823 823
824 824 def delete_tracked_hint(self):
825 825 """remove the tracked_hint file
826 826
827 827 To be used by format downgrades operation"""
828 828 self._opener.unlink(self._filename_th)
829 829 self._use_tracked_hint = False
830 830
831 831 def addparentchangecallback(self, category, callback):
832 832 """add a callback to be called when the wd parents are changed
833 833
834 834 Callback will be called with the following arguments:
835 835 dirstate, (oldp1, oldp2), (newp1, newp2)
836 836
837 837 Category is a unique identifier to allow overwriting an old callback
838 838 with a newer callback.
839 839 """
840 840 self._plchangecallbacks[category] = callback
841 841
842 842 def _writedirstate(self, tr, st):
843 843 # notify callbacks about parents change
844 844 if self._origpl is not None and self._origpl != self._pl:
845 845 for c, callback in sorted(self._plchangecallbacks.items()):
846 846 callback(self, self._origpl, self._pl)
847 847 self._origpl = None
848 848 self._map.write(tr, st)
849 849 self._dirty = False
850 850 self._dirty_tracked_set = False
851 851
852 852 def _write_tracked_hint(self, tr, f):
853 853 key = node.hex(uuid.uuid4().bytes)
854 854 f.write(b"1\n%s\n" % key) # 1 is the format version
855 855
856 856 def _dirignore(self, f):
857 857 if self._ignore(f):
858 858 return True
859 859 for p in pathutil.finddirs(f):
860 860 if self._ignore(p):
861 861 return True
862 862 return False
863 863
864 864 def _ignorefiles(self):
865 865 files = []
866 866 if os.path.exists(self._join(b'.hgignore')):
867 867 files.append(self._join(b'.hgignore'))
868 868 for name, path in self._ui.configitems(b"ui"):
869 869 if name == b'ignore' or name.startswith(b'ignore.'):
870 870 # we need to use os.path.join here rather than self._join
871 871 # because path is arbitrary and user-specified
872 872 files.append(os.path.join(self._rootdir, util.expandpath(path)))
873 873 return files
874 874
875 875 def _ignorefileandline(self, f):
876 876 files = collections.deque(self._ignorefiles())
877 877 visited = set()
878 878 while files:
879 879 i = files.popleft()
880 880 patterns = matchmod.readpatternfile(
881 881 i, self._ui.warn, sourceinfo=True
882 882 )
883 883 for pattern, lineno, line in patterns:
884 884 kind, p = matchmod._patsplit(pattern, b'glob')
885 885 if kind == b"subinclude":
886 886 if p not in visited:
887 887 files.append(p)
888 888 continue
889 889 m = matchmod.match(
890 890 self._root, b'', [], [pattern], warn=self._ui.warn
891 891 )
892 892 if m(f):
893 893 return (i, lineno, line)
894 894 visited.add(i)
895 895 return (None, -1, b"")
896 896
897 897 def _walkexplicit(self, match, subrepos):
898 898 """Get stat data about the files explicitly specified by match.
899 899
900 900 Return a triple (results, dirsfound, dirsnotfound).
901 901 - results is a mapping from filename to stat result. It also contains
902 902 listings mapping subrepos and .hg to None.
903 903 - dirsfound is a list of files found to be directories.
904 904 - dirsnotfound is a list of files that the dirstate thinks are
905 905 directories and that were not found."""
906 906
907 907 def badtype(mode):
908 908 kind = _(b'unknown')
909 909 if stat.S_ISCHR(mode):
910 910 kind = _(b'character device')
911 911 elif stat.S_ISBLK(mode):
912 912 kind = _(b'block device')
913 913 elif stat.S_ISFIFO(mode):
914 914 kind = _(b'fifo')
915 915 elif stat.S_ISSOCK(mode):
916 916 kind = _(b'socket')
917 917 elif stat.S_ISDIR(mode):
918 918 kind = _(b'directory')
919 919 return _(b'unsupported file type (type is %s)') % kind
920 920
921 921 badfn = match.bad
922 922 dmap = self._map
923 923 lstat = os.lstat
924 924 getkind = stat.S_IFMT
925 925 dirkind = stat.S_IFDIR
926 926 regkind = stat.S_IFREG
927 927 lnkkind = stat.S_IFLNK
928 928 join = self._join
929 929 dirsfound = []
930 930 foundadd = dirsfound.append
931 931 dirsnotfound = []
932 932 notfoundadd = dirsnotfound.append
933 933
934 934 if not match.isexact() and self._checkcase:
935 935 normalize = self._normalize
936 936 else:
937 937 normalize = None
938 938
939 939 files = sorted(match.files())
940 940 subrepos.sort()
941 941 i, j = 0, 0
942 942 while i < len(files) and j < len(subrepos):
943 943 subpath = subrepos[j] + b"/"
944 944 if files[i] < subpath:
945 945 i += 1
946 946 continue
947 947 while i < len(files) and files[i].startswith(subpath):
948 948 del files[i]
949 949 j += 1
950 950
951 951 if not files or b'' in files:
952 952 files = [b'']
953 953 # constructing the foldmap is expensive, so don't do it for the
954 954 # common case where files is ['']
955 955 normalize = None
956 956 results = dict.fromkeys(subrepos)
957 957 results[b'.hg'] = None
958 958
959 959 for ff in files:
960 960 if normalize:
961 961 nf = normalize(ff, False, True)
962 962 else:
963 963 nf = ff
964 964 if nf in results:
965 965 continue
966 966
967 967 try:
968 968 st = lstat(join(nf))
969 969 kind = getkind(st.st_mode)
970 970 if kind == dirkind:
971 971 if nf in dmap:
972 972 # file replaced by dir on disk but still in dirstate
973 973 results[nf] = None
974 974 foundadd((nf, ff))
975 975 elif kind == regkind or kind == lnkkind:
976 976 results[nf] = st
977 977 else:
978 978 badfn(ff, badtype(kind))
979 979 if nf in dmap:
980 980 results[nf] = None
981 981 except OSError as inst: # nf not found on disk - it is dirstate only
982 982 if nf in dmap: # does it exactly match a missing file?
983 983 results[nf] = None
984 984 else: # does it match a missing directory?
985 985 if self._map.hasdir(nf):
986 986 notfoundadd(nf)
987 987 else:
988 988 badfn(ff, encoding.strtolocal(inst.strerror))
989 989
990 990 # match.files() may contain explicitly-specified paths that shouldn't
991 991 # be taken; drop them from the list of files found. dirsfound/notfound
992 992 # aren't filtered here because they will be tested later.
993 993 if match.anypats():
994 994 for f in list(results):
995 995 if f == b'.hg' or f in subrepos:
996 996 # keep sentinel to disable further out-of-repo walks
997 997 continue
998 998 if not match(f):
999 999 del results[f]
1000 1000
1001 1001 # Case insensitive filesystems cannot rely on lstat() failing to detect
1002 1002 # a case-only rename. Prune the stat object for any file that does not
1003 1003 # match the case in the filesystem, if there are multiple files that
1004 1004 # normalize to the same path.
1005 1005 if match.isexact() and self._checkcase:
1006 1006 normed = {}
1007 1007
1008 1008 for f, st in results.items():
1009 1009 if st is None:
1010 1010 continue
1011 1011
1012 1012 nc = util.normcase(f)
1013 1013 paths = normed.get(nc)
1014 1014
1015 1015 if paths is None:
1016 1016 paths = set()
1017 1017 normed[nc] = paths
1018 1018
1019 1019 paths.add(f)
1020 1020
1021 1021 for norm, paths in normed.items():
1022 1022 if len(paths) > 1:
1023 1023 for path in paths:
1024 1024 folded = self._discoverpath(
1025 1025 path, norm, True, None, self._map.dirfoldmap
1026 1026 )
1027 1027 if path != folded:
1028 1028 results[path] = None
1029 1029
1030 1030 return results, dirsfound, dirsnotfound
1031 1031
1032 1032 def walk(self, match, subrepos, unknown, ignored, full=True):
1033 1033 """
1034 1034 Walk recursively through the directory tree, finding all files
1035 1035 matched by match.
1036 1036
1037 1037 If full is False, maybe skip some known-clean files.
1038 1038
1039 1039 Return a dict mapping filename to stat-like object (either
1040 1040 mercurial.osutil.stat instance or return value of os.stat()).
1041 1041
1042 1042 """
1043 1043 # full is a flag that extensions that hook into walk can use -- this
1044 1044 # implementation doesn't use it at all. This satisfies the contract
1045 1045 # because we only guarantee a "maybe".
1046 1046
1047 1047 if ignored:
1048 1048 ignore = util.never
1049 1049 dirignore = util.never
1050 1050 elif unknown:
1051 1051 ignore = self._ignore
1052 1052 dirignore = self._dirignore
1053 1053 else:
1054 1054 # if not unknown and not ignored, drop dir recursion and step 2
1055 1055 ignore = util.always
1056 1056 dirignore = util.always
1057 1057
1058 1058 if self._sparsematchfn is not None:
1059 1059 em = matchmod.exact(match.files())
1060 1060 sm = matchmod.unionmatcher([self._sparsematcher, em])
1061 1061 match = matchmod.intersectmatchers(match, sm)
1062 1062
1063 1063 matchfn = match.matchfn
1064 1064 matchalways = match.always()
1065 1065 matchtdir = match.traversedir
1066 1066 dmap = self._map
1067 1067 listdir = util.listdir
1068 1068 lstat = os.lstat
1069 1069 dirkind = stat.S_IFDIR
1070 1070 regkind = stat.S_IFREG
1071 1071 lnkkind = stat.S_IFLNK
1072 1072 join = self._join
1073 1073
1074 1074 exact = skipstep3 = False
1075 1075 if match.isexact(): # match.exact
1076 1076 exact = True
1077 1077 dirignore = util.always # skip step 2
1078 1078 elif match.prefix(): # match.match, no patterns
1079 1079 skipstep3 = True
1080 1080
1081 1081 if not exact and self._checkcase:
1082 1082 normalize = self._normalize
1083 1083 normalizefile = self._normalizefile
1084 1084 skipstep3 = False
1085 1085 else:
1086 1086 normalize = self._normalize
1087 1087 normalizefile = None
1088 1088
1089 1089 # step 1: find all explicit files
1090 1090 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1091 1091 if matchtdir:
1092 1092 for d in work:
1093 1093 matchtdir(d[0])
1094 1094 for d in dirsnotfound:
1095 1095 matchtdir(d)
1096 1096
1097 1097 skipstep3 = skipstep3 and not (work or dirsnotfound)
1098 1098 work = [d for d in work if not dirignore(d[0])]
1099 1099
1100 1100 # step 2: visit subdirectories
1101 1101 def traverse(work, alreadynormed):
1102 1102 wadd = work.append
1103 1103 while work:
1104 1104 tracing.counter('dirstate.walk work', len(work))
1105 1105 nd = work.pop()
1106 1106 visitentries = match.visitchildrenset(nd)
1107 1107 if not visitentries:
1108 1108 continue
1109 1109 if visitentries == b'this' or visitentries == b'all':
1110 1110 visitentries = None
1111 1111 skip = None
1112 1112 if nd != b'':
1113 1113 skip = b'.hg'
1114 1114 try:
1115 1115 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1116 1116 entries = listdir(join(nd), stat=True, skip=skip)
1117 1117 except (PermissionError, FileNotFoundError) as inst:
1118 1118 match.bad(
1119 1119 self.pathto(nd), encoding.strtolocal(inst.strerror)
1120 1120 )
1121 1121 continue
1122 1122 for f, kind, st in entries:
1123 1123 # Some matchers may return files in the visitentries set,
1124 1124 # instead of 'this', if the matcher explicitly mentions them
1125 1125 # and is not an exactmatcher. This is acceptable; we do not
1126 1126 # make any hard assumptions about file-or-directory below
1127 1127 # based on the presence of `f` in visitentries. If
1128 1128 # visitchildrenset returned a set, we can always skip the
1129 1129 # entries *not* in the set it provided regardless of whether
1130 1130 # they're actually a file or a directory.
1131 1131 if visitentries and f not in visitentries:
1132 1132 continue
1133 1133 if normalizefile:
1134 1134 # even though f might be a directory, we're only
1135 1135 # interested in comparing it to files currently in the
1136 1136 # dmap -- therefore normalizefile is enough
1137 1137 nf = normalizefile(
1138 1138 nd and (nd + b"/" + f) or f, True, True
1139 1139 )
1140 1140 else:
1141 1141 nf = nd and (nd + b"/" + f) or f
1142 1142 if nf not in results:
1143 1143 if kind == dirkind:
1144 1144 if not ignore(nf):
1145 1145 if matchtdir:
1146 1146 matchtdir(nf)
1147 1147 wadd(nf)
1148 1148 if nf in dmap and (matchalways or matchfn(nf)):
1149 1149 results[nf] = None
1150 1150 elif kind == regkind or kind == lnkkind:
1151 1151 if nf in dmap:
1152 1152 if matchalways or matchfn(nf):
1153 1153 results[nf] = st
1154 1154 elif (matchalways or matchfn(nf)) and not ignore(
1155 1155 nf
1156 1156 ):
1157 1157 # unknown file -- normalize if necessary
1158 1158 if not alreadynormed:
1159 1159 nf = normalize(nf, False, True)
1160 1160 results[nf] = st
1161 1161 elif nf in dmap and (matchalways or matchfn(nf)):
1162 1162 results[nf] = None
1163 1163
1164 1164 for nd, d in work:
1165 1165 # alreadynormed means that processwork doesn't have to do any
1166 1166 # expensive directory normalization
1167 1167 alreadynormed = not normalize or nd == d
1168 1168 traverse([d], alreadynormed)
1169 1169
1170 1170 for s in subrepos:
1171 1171 del results[s]
1172 1172 del results[b'.hg']
1173 1173
1174 1174 # step 3: visit remaining files from dmap
1175 1175 if not skipstep3 and not exact:
1176 1176 # If a dmap file is not in results yet, it was either
1177 1177 # a) not matching matchfn b) ignored, c) missing, or d) under a
1178 1178 # symlink directory.
1179 1179 if not results and matchalways:
1180 1180 visit = [f for f in dmap]
1181 1181 else:
1182 1182 visit = [f for f in dmap if f not in results and matchfn(f)]
1183 1183 visit.sort()
1184 1184
1185 1185 if unknown:
1186 1186 # unknown == True means we walked all dirs under the roots
1187 1187 # that wasn't ignored, and everything that matched was stat'ed
1188 1188 # and is already in results.
1189 1189 # The rest must thus be ignored or under a symlink.
1190 1190 audit_path = pathutil.pathauditor(self._root, cached=True)
1191 1191
1192 1192 for nf in iter(visit):
1193 1193 # If a stat for the same file was already added with a
1194 1194 # different case, don't add one for this, since that would
1195 1195 # make it appear as if the file exists under both names
1196 1196 # on disk.
1197 1197 if (
1198 1198 normalizefile
1199 1199 and normalizefile(nf, True, True) in results
1200 1200 ):
1201 1201 results[nf] = None
1202 1202 # Report ignored items in the dmap as long as they are not
1203 1203 # under a symlink directory.
1204 1204 elif audit_path.check(nf):
1205 1205 try:
1206 1206 results[nf] = lstat(join(nf))
1207 1207 # file was just ignored, no links, and exists
1208 1208 except OSError:
1209 1209 # file doesn't exist
1210 1210 results[nf] = None
1211 1211 else:
1212 1212 # It's either missing or under a symlink directory
1213 1213 # which we in this case report as missing
1214 1214 results[nf] = None
1215 1215 else:
1216 1216 # We may not have walked the full directory tree above,
1217 1217 # so stat and check everything we missed.
1218 1218 iv = iter(visit)
1219 1219 for st in util.statfiles([join(i) for i in visit]):
1220 1220 results[next(iv)] = st
1221 1221 return results
1222 1222
1223 1223 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1224 1224 if self._sparsematchfn is not None:
1225 1225 em = matchmod.exact(matcher.files())
1226 1226 sm = matchmod.unionmatcher([self._sparsematcher, em])
1227 1227 matcher = matchmod.intersectmatchers(matcher, sm)
1228 1228 # Force Rayon (Rust parallelism library) to respect the number of
1229 1229 # workers. This is a temporary workaround until Rust code knows
1230 1230 # how to read the config file.
1231 1231 numcpus = self._ui.configint(b"worker", b"numcpus")
1232 1232 if numcpus is not None:
1233 1233 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1234 1234
1235 1235 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1236 1236 if not workers_enabled:
1237 1237 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1238 1238
1239 1239 (
1240 1240 lookup,
1241 1241 modified,
1242 1242 added,
1243 1243 removed,
1244 1244 deleted,
1245 1245 clean,
1246 1246 ignored,
1247 1247 unknown,
1248 1248 warnings,
1249 1249 bad,
1250 1250 traversed,
1251 1251 dirty,
1252 1252 ) = rustmod.status(
1253 1253 self._map._map,
1254 1254 matcher,
1255 1255 self._rootdir,
1256 1256 self._ignorefiles(),
1257 1257 self._checkexec,
1258 1258 bool(list_clean),
1259 1259 bool(list_ignored),
1260 1260 bool(list_unknown),
1261 1261 bool(matcher.traversedir),
1262 1262 )
1263 1263
1264 1264 self._dirty |= dirty
1265 1265
1266 1266 if matcher.traversedir:
1267 1267 for dir in traversed:
1268 1268 matcher.traversedir(dir)
1269 1269
1270 1270 if self._ui.warn:
1271 1271 for item in warnings:
1272 1272 if isinstance(item, tuple):
1273 1273 file_path, syntax = item
1274 1274 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1275 1275 file_path,
1276 1276 syntax,
1277 1277 )
1278 1278 self._ui.warn(msg)
1279 1279 else:
1280 1280 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1281 1281 self._ui.warn(
1282 1282 msg
1283 1283 % (
1284 1284 pathutil.canonpath(
1285 1285 self._rootdir, self._rootdir, item
1286 1286 ),
1287 1287 b"No such file or directory",
1288 1288 )
1289 1289 )
1290 1290
1291 1291 for (fn, message) in bad:
1292 1292 matcher.bad(fn, encoding.strtolocal(message))
1293 1293
1294 1294 status = scmutil.status(
1295 1295 modified=modified,
1296 1296 added=added,
1297 1297 removed=removed,
1298 1298 deleted=deleted,
1299 1299 unknown=unknown,
1300 1300 ignored=ignored,
1301 1301 clean=clean,
1302 1302 )
1303 1303 return (lookup, status)
1304 1304
1305 1305 def status(self, match, subrepos, ignored, clean, unknown):
1306 1306 """Determine the status of the working copy relative to the
1307 1307 dirstate and return a pair of (unsure, status), where status is of type
1308 1308 scmutil.status and:
1309 1309
1310 1310 unsure:
1311 1311 files that might have been modified since the dirstate was
1312 1312 written, but need to be read to be sure (size is the same
1313 1313 but mtime differs)
1314 1314 status.modified:
1315 1315 files that have definitely been modified since the dirstate
1316 1316 was written (different size or mode)
1317 1317 status.clean:
1318 1318 files that have definitely not been modified since the
1319 1319 dirstate was written
1320 1320 """
1321 1321 listignored, listclean, listunknown = ignored, clean, unknown
1322 1322 lookup, modified, added, unknown, ignored = [], [], [], [], []
1323 1323 removed, deleted, clean = [], [], []
1324 1324
1325 1325 dmap = self._map
1326 1326 dmap.preload()
1327 1327
1328 1328 use_rust = True
1329 1329
1330 1330 allowed_matchers = (
1331 1331 matchmod.alwaysmatcher,
1332 1332 matchmod.differencematcher,
1333 1333 matchmod.exactmatcher,
1334 1334 matchmod.includematcher,
1335 1335 matchmod.intersectionmatcher,
1336 1336 matchmod.nevermatcher,
1337 1337 matchmod.unionmatcher,
1338 1338 )
1339 1339
1340 1340 if rustmod is None:
1341 1341 use_rust = False
1342 1342 elif self._checkcase:
1343 1343 # Case-insensitive filesystems are not handled yet
1344 1344 use_rust = False
1345 1345 elif subrepos:
1346 1346 use_rust = False
1347 1347 elif not isinstance(match, allowed_matchers):
1348 1348 # Some matchers have yet to be implemented
1349 1349 use_rust = False
1350 1350
1351 1351 # Get the time from the filesystem so we can disambiguate files that
1352 1352 # appear modified in the present or future.
1353 1353 try:
1354 1354 mtime_boundary = timestamp.get_fs_now(self._opener)
1355 1355 except OSError:
1356 1356 # In largefiles or readonly context
1357 1357 mtime_boundary = None
1358 1358
1359 1359 if use_rust:
1360 1360 try:
1361 1361 res = self._rust_status(
1362 1362 match, listclean, listignored, listunknown
1363 1363 )
1364 1364 return res + (mtime_boundary,)
1365 1365 except rustmod.FallbackError:
1366 1366 pass
1367 1367
1368 1368 def noop(f):
1369 1369 pass
1370 1370
1371 1371 dcontains = dmap.__contains__
1372 1372 dget = dmap.__getitem__
1373 1373 ladd = lookup.append # aka "unsure"
1374 1374 madd = modified.append
1375 1375 aadd = added.append
1376 1376 uadd = unknown.append if listunknown else noop
1377 1377 iadd = ignored.append if listignored else noop
1378 1378 radd = removed.append
1379 1379 dadd = deleted.append
1380 1380 cadd = clean.append if listclean else noop
1381 1381 mexact = match.exact
1382 1382 dirignore = self._dirignore
1383 1383 checkexec = self._checkexec
1384 1384 checklink = self._checklink
1385 1385 copymap = self._map.copymap
1386 1386
1387 1387 # We need to do full walks when either
1388 1388 # - we're listing all clean files, or
1389 1389 # - match.traversedir does something, because match.traversedir should
1390 1390 # be called for every dir in the working dir
1391 1391 full = listclean or match.traversedir is not None
1392 1392 for fn, st in self.walk(
1393 1393 match, subrepos, listunknown, listignored, full=full
1394 1394 ).items():
1395 1395 if not dcontains(fn):
1396 1396 if (listignored or mexact(fn)) and dirignore(fn):
1397 1397 if listignored:
1398 1398 iadd(fn)
1399 1399 else:
1400 1400 uadd(fn)
1401 1401 continue
1402 1402
1403 1403 t = dget(fn)
1404 1404 mode = t.mode
1405 1405 size = t.size
1406 1406
1407 1407 if not st and t.tracked:
1408 1408 dadd(fn)
1409 1409 elif t.p2_info:
1410 1410 madd(fn)
1411 1411 elif t.added:
1412 1412 aadd(fn)
1413 1413 elif t.removed:
1414 1414 radd(fn)
1415 1415 elif t.tracked:
1416 1416 if not checklink and t.has_fallback_symlink:
1417 1417 # If the file system does not support symlink, the mode
1418 1418 # might not be correctly stored in the dirstate, so do not
1419 1419 # trust it.
1420 1420 ladd(fn)
1421 1421 elif not checkexec and t.has_fallback_exec:
1422 1422 # If the file system does not support exec bits, the mode
1423 1423 # might not be correctly stored in the dirstate, so do not
1424 1424 # trust it.
1425 1425 ladd(fn)
1426 1426 elif (
1427 1427 size >= 0
1428 1428 and (
1429 1429 (size != st.st_size and size != st.st_size & _rangemask)
1430 1430 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1431 1431 )
1432 1432 or fn in copymap
1433 1433 ):
1434 1434 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1435 1435 # issue6456: Size returned may be longer due to
1436 1436 # encryption on EXT-4 fscrypt, undecided.
1437 1437 ladd(fn)
1438 1438 else:
1439 1439 madd(fn)
1440 1440 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1441 1441 # There might be a change in the future if for example the
1442 1442 # internal clock is off, but this is a case where the issues
1443 1443 # the user would face would be a lot worse and there is
1444 1444 # nothing we can really do.
1445 1445 ladd(fn)
1446 1446 elif listclean:
1447 1447 cadd(fn)
1448 1448 status = scmutil.status(
1449 1449 modified, added, removed, deleted, unknown, ignored, clean
1450 1450 )
1451 1451 return (lookup, status, mtime_boundary)
1452 1452
1453 1453 def matches(self, match):
1454 1454 """
1455 1455 return files in the dirstate (in whatever state) filtered by match
1456 1456 """
1457 1457 dmap = self._map
1458 1458 if rustmod is not None:
1459 1459 dmap = self._map._map
1460 1460
1461 1461 if match.always():
1462 1462 return dmap.keys()
1463 1463 files = match.files()
1464 1464 if match.isexact():
1465 1465 # fast path -- filter the other way around, since typically files is
1466 1466 # much smaller than dmap
1467 1467 return [f for f in files if f in dmap]
1468 1468 if match.prefix() and all(fn in dmap for fn in files):
1469 1469 # fast path -- all the values are known to be files, so just return
1470 1470 # that
1471 1471 return list(files)
1472 1472 return [f for f in dmap if match(f)]
1473 1473
1474 1474 def _actualfilename(self, tr):
1475 1475 if tr:
1476 1476 return self._pendingfilename
1477 1477 else:
1478 1478 return self._filename
1479 1479
1480 1480 def data_backup_filename(self, backupname):
1481 1481 if not self._use_dirstate_v2:
1482 1482 return None
1483 1483 return backupname + b'.v2-data'
1484 1484
1485 1485 def _new_backup_data_filename(self, backupname):
1486 1486 """return a filename to backup a data-file or None"""
1487 1487 if not self._use_dirstate_v2:
1488 1488 return None
1489 if self._map.docket.uuid is None:
1490 # not created yet, nothing to backup
1491 return None
1489 1492 data_filename = self._map.docket.data_filename()
1490 1493 return data_filename, self.data_backup_filename(backupname)
1491 1494
1492 1495 def backup_data_file(self, backupname):
1493 1496 if not self._use_dirstate_v2:
1494 1497 return None
1495 1498 docket = docketmod.DirstateDocket.parse(
1496 1499 self._opener.read(backupname),
1497 1500 self._nodeconstants,
1498 1501 )
1499 1502 return self.data_backup_filename(backupname), docket.data_filename()
1500 1503
1501 1504 def savebackup(self, tr, backupname):
1502 1505 '''Save current dirstate into backup file'''
1503 1506 filename = self._actualfilename(tr)
1504 1507 assert backupname != filename
1505 1508
1506 1509 # use '_writedirstate' instead of 'write' to write changes certainly,
1507 1510 # because the latter omits writing out if transaction is running.
1508 1511 # output file will be used to create backup of dirstate at this point.
1509 if self._dirty or not self._opener.exists(filename):
1512 if self._dirty:
1510 1513 self._writedirstate(
1511 1514 tr,
1512 1515 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1513 1516 )
1514 1517
1515 1518 if tr:
1516 1519 # ensure that subsequent tr.writepending returns True for
1517 1520 # changes written out above, even if dirstate is never
1518 1521 # changed after this
1519 1522 tr.addfilegenerator(
1520 1523 b'dirstate-1-main',
1521 1524 (self._filename,),
1522 1525 lambda f: self._writedirstate(tr, f),
1523 1526 location=b'plain',
1524 1527 post_finalize=True,
1525 1528 )
1526 1529
1527 1530 # ensure that pending file written above is unlinked at
1528 1531 # failure, even if tr.writepending isn't invoked until the
1529 1532 # end of this transaction
1530 1533 tr.registertmp(filename, location=b'plain')
1531 1534
1532 1535 self._opener.tryunlink(backupname)
1533 if True:
1536 if self._opener.exists(filename):
1534 1537 # hardlink backup is okay because _writedirstate is always called
1535 1538 # with an "atomictemp=True" file.
1536 1539 util.copyfile(
1537 1540 self._opener.join(filename),
1538 1541 self._opener.join(backupname),
1539 1542 hardlink=True,
1540 1543 )
1541 1544 data_pair = self._new_backup_data_filename(backupname)
1542 1545 if data_pair is not None:
1543 1546 data_filename, bck_data_filename = data_pair
1544 1547 util.copyfile(
1545 1548 self._opener.join(data_filename),
1546 1549 self._opener.join(bck_data_filename),
1547 1550 hardlink=True,
1548 1551 )
1549 1552 if tr is not None:
1550 1553 # ensure that pending file written above is unlinked at
1551 1554 # failure, even if tr.writepending isn't invoked until the
1552 1555 # end of this transaction
1553 1556 tr.registertmp(bck_data_filename, location=b'plain')
1554 1557
1555 1558 def restorebackup(self, tr, backupname):
1556 1559 '''Restore dirstate by backup file'''
1557 1560 # this "invalidate()" prevents "wlock.release()" from writing
1558 1561 # changes of dirstate out after restoring from backup file
1559 1562 self.invalidate()
1563 o = self._opener
1564 if not o.exists(backupname):
1565 # there was no file backup, delete existing files
1560 1566 filename = self._actualfilename(tr)
1561 o = self._opener
1567 data_file = None
1568 if self._use_dirstate_v2:
1569 data_file = self._map.docket.data_filename()
1570 if o.exists(filename):
1571 o.unlink(filename)
1572 if data_file is not None and o.exists(data_file):
1573 o.unlink(data_file)
1574 return
1575 filename = self._actualfilename(tr)
1562 1576 data_pair = self.backup_data_file(backupname)
1563 if util.samefile(o.join(backupname), o.join(filename)):
1577 if o.exists(filename) and util.samefile(
1578 o.join(backupname), o.join(filename)
1579 ):
1564 1580 o.unlink(backupname)
1565 1581 else:
1566 1582 o.rename(backupname, filename, checkambig=True)
1567 1583
1568 1584 if data_pair is not None:
1569 1585 data_backup, target = data_pair
1570 1586 if o.exists(target) and util.samefile(
1571 1587 o.join(data_backup), o.join(target)
1572 1588 ):
1573 1589 o.unlink(data_backup)
1574 1590 else:
1575 1591 o.rename(data_backup, target, checkambig=True)
1576 1592
1577 1593 def clearbackup(self, tr, backupname):
1578 1594 '''Clear backup file'''
1579 1595 o = self._opener
1596 if o.exists(backupname):
1580 1597 data_backup = self.backup_data_file(backupname)
1581 1598 o.unlink(backupname)
1582
1583 1599 if data_backup is not None:
1584 1600 o.unlink(data_backup[0])
1585 1601
1586 1602 def verify(self, m1, m2, p1, narrow_matcher=None):
1587 1603 """
1588 1604 check the dirstate contents against the parent manifest and yield errors
1589 1605 """
1590 1606 missing_from_p1 = _(
1591 1607 b"%s marked as tracked in p1 (%s) but not in manifest1\n"
1592 1608 )
1593 1609 unexpected_in_p1 = _(b"%s marked as added, but also in manifest1\n")
1594 1610 missing_from_ps = _(
1595 1611 b"%s marked as modified, but not in either manifest\n"
1596 1612 )
1597 1613 missing_from_ds = _(
1598 1614 b"%s in manifest1, but not marked as tracked in p1 (%s)\n"
1599 1615 )
1600 1616 for f, entry in self.items():
1601 1617 if entry.p1_tracked:
1602 1618 if entry.modified and f not in m1 and f not in m2:
1603 1619 yield missing_from_ps % f
1604 1620 elif f not in m1:
1605 1621 yield missing_from_p1 % (f, node.short(p1))
1606 1622 if entry.added and f in m1:
1607 1623 yield unexpected_in_p1 % f
1608 1624 for f in m1:
1609 1625 if narrow_matcher is not None and not narrow_matcher(f):
1610 1626 continue
1611 1627 entry = self.get_entry(f)
1612 1628 if not entry.p1_tracked:
1613 1629 yield missing_from_ds % (f, node.short(p1))
@@ -1,50 +1,49 b''
1 1 Create an empty repo:
2 2
3 3 $ hg init a
4 4 $ cd a
5 5
6 6 Try some commands:
7 7
8 8 $ hg log
9 9 $ hg grep wah
10 10 [1]
11 11 $ hg manifest
12 12 $ hg verify -q
13 13
14 14 Check the basic files created:
15 15
16 16 $ ls .hg
17 17 00changelog.i
18 18 cache
19 19 requires
20 20 store
21 21 wcache
22 22
23 23 Should be empty (except for the "basic" requires):
24 24
25 25 $ ls .hg/store
26 26 requires
27 27
28 28 Poke at a clone:
29 29
30 30 $ cd ..
31 31 $ hg clone a b
32 32 updating to branch default
33 33 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
34 34 $ cd b
35 35 $ hg verify -q
36 36 $ ls .hg
37 37 00changelog.i
38 38 cache
39 dirstate
40 39 hgrc
41 40 requires
42 41 store
43 42 wcache
44 43
45 44 Should be empty (except for the "basic" requires):
46 45
47 46 $ ls .hg/store
48 47 requires
49 48
50 49 $ cd ..
@@ -1,186 +1,184 b''
1 1 #require unix-permissions
2 2
3 3 test that new files created in .hg inherit the permissions from .hg/store
4 4
5 5 $ mkdir dir
6 6
7 7 just in case somebody has a strange $TMPDIR
8 8
9 9 $ chmod g-s dir
10 10 $ cd dir
11 11
12 12 $ cat >printmodes.py <<EOF
13 13 > import os
14 14 > import sys
15 15 >
16 16 > allnames = []
17 17 > isdir = {}
18 18 > for root, dirs, files in os.walk(sys.argv[1]):
19 19 > for d in dirs:
20 20 > name = os.path.join(root, d)
21 21 > isdir[name] = 1
22 22 > allnames.append(name)
23 23 > for f in files:
24 24 > name = os.path.join(root, f)
25 25 > allnames.append(name)
26 26 > allnames.sort()
27 27 > for name in allnames:
28 28 > suffix = name in isdir and '/' or ''
29 29 > print('%05o %s%s' % (os.lstat(name).st_mode & 0o7777, name, suffix))
30 30 > EOF
31 31
32 32 $ cat >mode.py <<EOF
33 33 > import os
34 34 > import sys
35 35 > print('%05o' % os.lstat(sys.argv[1]).st_mode)
36 36 > EOF
37 37
38 38 $ umask 077
39 39
40 40 $ hg init repo
41 41 $ cd repo
42 42
43 43 $ chmod 0770 .hg/store .hg/cache .hg/wcache
44 44
45 45 before commit
46 46 store can be written by the group, other files cannot
47 47 store is setgid
48 48
49 49 $ "$PYTHON" ../printmodes.py .
50 50 00700 ./.hg/
51 51 00600 ./.hg/00changelog.i
52 52 00770 ./.hg/cache/
53 53 00600 ./.hg/requires
54 54 00770 ./.hg/store/
55 55 00600 ./.hg/store/requires
56 56 00770 ./.hg/wcache/
57 57
58 58 $ mkdir dir
59 59 $ touch foo dir/bar
60 60 $ hg ci -qAm 'add files'
61 61
62 62 after commit
63 63 working dir files can only be written by the owner
64 64 files created in .hg can be written by the group
65 65 (in particular, store/**, dirstate, branch cache file, undo files)
66 66 new directories are setgid
67 67
68 68 $ "$PYTHON" ../printmodes.py .
69 69 00700 ./.hg/
70 70 00600 ./.hg/00changelog.i
71 71 00770 ./.hg/cache/
72 72 00660 ./.hg/cache/branch2-served
73 73 00660 ./.hg/cache/rbc-names-v1
74 74 00660 ./.hg/cache/rbc-revs-v1
75 75 00660 ./.hg/dirstate
76 76 00660 ./.hg/fsmonitor.state (fsmonitor !)
77 77 00660 ./.hg/last-message.txt
78 78 00600 ./.hg/requires
79 79 00770 ./.hg/store/
80 80 00660 ./.hg/store/00changelog.i
81 81 00660 ./.hg/store/00manifest.i
82 82 00770 ./.hg/store/data/
83 83 00770 ./.hg/store/data/dir/
84 84 00660 ./.hg/store/data/dir/bar.i (reporevlogstore !)
85 85 00660 ./.hg/store/data/foo.i (reporevlogstore !)
86 86 00770 ./.hg/store/data/dir/bar/ (reposimplestore !)
87 87 00660 ./.hg/store/data/dir/bar/b80de5d138758541c5f05265ad144ab9fa86d1db (reposimplestore !)
88 88 00660 ./.hg/store/data/dir/bar/index (reposimplestore !)
89 89 00770 ./.hg/store/data/foo/ (reposimplestore !)
90 90 00660 ./.hg/store/data/foo/b80de5d138758541c5f05265ad144ab9fa86d1db (reposimplestore !)
91 91 00660 ./.hg/store/data/foo/index (reposimplestore !)
92 92 00660 ./.hg/store/fncache (repofncache !)
93 93 00660 ./.hg/store/phaseroots
94 94 00600 ./.hg/store/requires
95 95 00660 ./.hg/store/undo
96 96 00660 ./.hg/store/undo.backupfiles
97 97 00660 ./.hg/store/undo.phaseroots
98 98 00660 ./.hg/undo.backup.dirstate
99 99 00660 ./.hg/undo.bookmarks
100 100 00660 ./.hg/undo.branch
101 101 00660 ./.hg/undo.desc
102 102 00660 ./.hg/undo.dirstate
103 103 00770 ./.hg/wcache/
104 104 00711 ./.hg/wcache/checkisexec
105 105 007.. ./.hg/wcache/checklink (re)
106 106 00600 ./.hg/wcache/checklink-target
107 107 00660 ./.hg/wcache/manifestfulltextcache (reporevlogstore !)
108 108 00700 ./dir/
109 109 00600 ./dir/bar
110 110 00600 ./foo
111 111
112 112 $ umask 007
113 113 $ hg init ../push
114 114
115 115 before push
116 116 group can write everything
117 117
118 118 $ "$PYTHON" ../printmodes.py ../push
119 119 00770 ../push/.hg/
120 120 00660 ../push/.hg/00changelog.i
121 121 00770 ../push/.hg/cache/
122 122 00660 ../push/.hg/requires
123 123 00770 ../push/.hg/store/
124 124 00660 ../push/.hg/store/requires
125 125 00770 ../push/.hg/wcache/
126 126
127 127 $ umask 077
128 128 $ hg -q push ../push
129 129
130 130 after push
131 131 group can still write everything
132 132
133 133 $ "$PYTHON" ../printmodes.py ../push
134 134 00770 ../push/.hg/
135 135 00660 ../push/.hg/00changelog.i
136 136 00770 ../push/.hg/cache/
137 137 00660 ../push/.hg/cache/branch2-base
138 138 00660 ../push/.hg/cache/rbc-names-v1
139 139 00660 ../push/.hg/cache/rbc-revs-v1
140 00660 ../push/.hg/dirstate
141 140 00660 ../push/.hg/requires
142 141 00770 ../push/.hg/store/
143 142 00660 ../push/.hg/store/00changelog.i
144 143 00660 ../push/.hg/store/00manifest.i
145 144 00770 ../push/.hg/store/data/
146 145 00770 ../push/.hg/store/data/dir/
147 146 00660 ../push/.hg/store/data/dir/bar.i (reporevlogstore !)
148 147 00660 ../push/.hg/store/data/foo.i (reporevlogstore !)
149 148 00770 ../push/.hg/store/data/dir/bar/ (reposimplestore !)
150 149 00660 ../push/.hg/store/data/dir/bar/b80de5d138758541c5f05265ad144ab9fa86d1db (reposimplestore !)
151 150 00660 ../push/.hg/store/data/dir/bar/index (reposimplestore !)
152 151 00770 ../push/.hg/store/data/foo/ (reposimplestore !)
153 152 00660 ../push/.hg/store/data/foo/b80de5d138758541c5f05265ad144ab9fa86d1db (reposimplestore !)
154 153 00660 ../push/.hg/store/data/foo/index (reposimplestore !)
155 154 00660 ../push/.hg/store/fncache (repofncache !)
156 155 00660 ../push/.hg/store/requires
157 156 00660 ../push/.hg/store/undo
158 157 00660 ../push/.hg/store/undo.backupfiles
159 158 00660 ../push/.hg/store/undo.phaseroots
160 159 00660 ../push/.hg/undo.bookmarks
161 160 00660 ../push/.hg/undo.branch
162 161 00660 ../push/.hg/undo.desc
163 00660 ../push/.hg/undo.dirstate
164 162 00770 ../push/.hg/wcache/
165 163
166 164
167 165 Test that we don't lose the setgid bit when we call chmod.
168 166 Not all systems support setgid directories (e.g. HFS+), so
169 167 just check that directories have the same mode.
170 168
171 169 $ cd ..
172 170 $ hg init setgid
173 171 $ cd setgid
174 172 $ chmod g+rwx .hg/store
175 173 $ chmod g+s .hg/store 2> /dev/null || true
176 174 $ mkdir dir
177 175 $ touch dir/file
178 176 $ hg ci -qAm 'add dir/file'
179 177 $ storemode=`"$PYTHON" ../mode.py .hg/store`
180 178 $ dirmode=`"$PYTHON" ../mode.py .hg/store/data/dir`
181 179 $ if [ "$storemode" != "$dirmode" ]; then
182 180 > echo "$storemode != $dirmode"
183 181 > fi
184 182 $ cd ..
185 183
186 184 $ cd .. # g-s dir
General Comments 0
You need to be logged in to leave comments. Login now