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