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