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