##// END OF EJS Templates
dirstate: infer the 'n' state from `from_p2`...
marmoute -
r48318:d3cf2032 default
parent child Browse files
Show More
@@ -1,1439 +1,1439 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 dirstatetuple = parsers.dirstatetuple
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 @interfaceutil.implementer(intdirstate.idirstate)
76 76 class dirstate(object):
77 77 def __init__(
78 78 self,
79 79 opener,
80 80 ui,
81 81 root,
82 82 validate,
83 83 sparsematchfn,
84 84 nodeconstants,
85 85 use_dirstate_v2,
86 86 ):
87 87 """Create a new dirstate object.
88 88
89 89 opener is an open()-like callable that can be used to open the
90 90 dirstate file; root is the root of the directory tracked by
91 91 the dirstate.
92 92 """
93 93 self._use_dirstate_v2 = use_dirstate_v2
94 94 self._nodeconstants = nodeconstants
95 95 self._opener = opener
96 96 self._validate = validate
97 97 self._root = root
98 98 self._sparsematchfn = sparsematchfn
99 99 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
100 100 # UNC path pointing to root share (issue4557)
101 101 self._rootdir = pathutil.normasprefix(root)
102 102 self._dirty = False
103 103 self._lastnormaltime = 0
104 104 self._ui = ui
105 105 self._filecache = {}
106 106 self._parentwriters = 0
107 107 self._filename = b'dirstate'
108 108 self._pendingfilename = b'%s.pending' % self._filename
109 109 self._plchangecallbacks = {}
110 110 self._origpl = None
111 111 self._updatedfiles = set()
112 112 self._mapcls = dirstatemap.dirstatemap
113 113 # Access and cache cwd early, so we don't access it for the first time
114 114 # after a working-copy update caused it to not exist (accessing it then
115 115 # raises an exception).
116 116 self._cwd
117 117
118 118 def prefetch_parents(self):
119 119 """make sure the parents are loaded
120 120
121 121 Used to avoid a race condition.
122 122 """
123 123 self._pl
124 124
125 125 @contextlib.contextmanager
126 126 def parentchange(self):
127 127 """Context manager for handling dirstate parents.
128 128
129 129 If an exception occurs in the scope of the context manager,
130 130 the incoherent dirstate won't be written when wlock is
131 131 released.
132 132 """
133 133 self._parentwriters += 1
134 134 yield
135 135 # Typically we want the "undo" step of a context manager in a
136 136 # finally block so it happens even when an exception
137 137 # occurs. In this case, however, we only want to decrement
138 138 # parentwriters if the code in the with statement exits
139 139 # normally, so we don't have a try/finally here on purpose.
140 140 self._parentwriters -= 1
141 141
142 142 def pendingparentchange(self):
143 143 """Returns true if the dirstate is in the middle of a set of changes
144 144 that modify the dirstate parent.
145 145 """
146 146 return self._parentwriters > 0
147 147
148 148 @propertycache
149 149 def _map(self):
150 150 """Return the dirstate contents (see documentation for dirstatemap)."""
151 151 self._map = self._mapcls(
152 152 self._ui,
153 153 self._opener,
154 154 self._root,
155 155 self._nodeconstants,
156 156 self._use_dirstate_v2,
157 157 )
158 158 return self._map
159 159
160 160 @property
161 161 def _sparsematcher(self):
162 162 """The matcher for the sparse checkout.
163 163
164 164 The working directory may not include every file from a manifest. The
165 165 matcher obtained by this property will match a path if it is to be
166 166 included in the working directory.
167 167 """
168 168 # TODO there is potential to cache this property. For now, the matcher
169 169 # is resolved on every access. (But the called function does use a
170 170 # cache to keep the lookup fast.)
171 171 return self._sparsematchfn()
172 172
173 173 @repocache(b'branch')
174 174 def _branch(self):
175 175 try:
176 176 return self._opener.read(b"branch").strip() or b"default"
177 177 except IOError as inst:
178 178 if inst.errno != errno.ENOENT:
179 179 raise
180 180 return b"default"
181 181
182 182 @property
183 183 def _pl(self):
184 184 return self._map.parents()
185 185
186 186 def hasdir(self, d):
187 187 return self._map.hastrackeddir(d)
188 188
189 189 @rootcache(b'.hgignore')
190 190 def _ignore(self):
191 191 files = self._ignorefiles()
192 192 if not files:
193 193 return matchmod.never()
194 194
195 195 pats = [b'include:%s' % f for f in files]
196 196 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
197 197
198 198 @propertycache
199 199 def _slash(self):
200 200 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
201 201
202 202 @propertycache
203 203 def _checklink(self):
204 204 return util.checklink(self._root)
205 205
206 206 @propertycache
207 207 def _checkexec(self):
208 208 return bool(util.checkexec(self._root))
209 209
210 210 @propertycache
211 211 def _checkcase(self):
212 212 return not util.fscasesensitive(self._join(b'.hg'))
213 213
214 214 def _join(self, f):
215 215 # much faster than os.path.join()
216 216 # it's safe because f is always a relative path
217 217 return self._rootdir + f
218 218
219 219 def flagfunc(self, buildfallback):
220 220 if self._checklink and self._checkexec:
221 221
222 222 def f(x):
223 223 try:
224 224 st = os.lstat(self._join(x))
225 225 if util.statislink(st):
226 226 return b'l'
227 227 if util.statisexec(st):
228 228 return b'x'
229 229 except OSError:
230 230 pass
231 231 return b''
232 232
233 233 return f
234 234
235 235 fallback = buildfallback()
236 236 if self._checklink:
237 237
238 238 def f(x):
239 239 if os.path.islink(self._join(x)):
240 240 return b'l'
241 241 if b'x' in fallback(x):
242 242 return b'x'
243 243 return b''
244 244
245 245 return f
246 246 if self._checkexec:
247 247
248 248 def f(x):
249 249 if b'l' in fallback(x):
250 250 return b'l'
251 251 if util.isexec(self._join(x)):
252 252 return b'x'
253 253 return b''
254 254
255 255 return f
256 256 else:
257 257 return fallback
258 258
259 259 @propertycache
260 260 def _cwd(self):
261 261 # internal config: ui.forcecwd
262 262 forcecwd = self._ui.config(b'ui', b'forcecwd')
263 263 if forcecwd:
264 264 return forcecwd
265 265 return encoding.getcwd()
266 266
267 267 def getcwd(self):
268 268 """Return the path from which a canonical path is calculated.
269 269
270 270 This path should be used to resolve file patterns or to convert
271 271 canonical paths back to file paths for display. It shouldn't be
272 272 used to get real file paths. Use vfs functions instead.
273 273 """
274 274 cwd = self._cwd
275 275 if cwd == self._root:
276 276 return b''
277 277 # self._root ends with a path separator if self._root is '/' or 'C:\'
278 278 rootsep = self._root
279 279 if not util.endswithsep(rootsep):
280 280 rootsep += pycompat.ossep
281 281 if cwd.startswith(rootsep):
282 282 return cwd[len(rootsep) :]
283 283 else:
284 284 # we're outside the repo. return an absolute path.
285 285 return cwd
286 286
287 287 def pathto(self, f, cwd=None):
288 288 if cwd is None:
289 289 cwd = self.getcwd()
290 290 path = util.pathto(self._root, cwd, f)
291 291 if self._slash:
292 292 return util.pconvert(path)
293 293 return path
294 294
295 295 def __getitem__(self, key):
296 296 """Return the current state of key (a filename) in the dirstate.
297 297
298 298 States are:
299 299 n normal
300 300 m needs merging
301 301 r marked for removal
302 302 a marked for addition
303 303 ? not tracked
304 304
305 305 XXX The "state" is a bit obscure to be in the "public" API. we should
306 306 consider migrating all user of this to going through the dirstate entry
307 307 instead.
308 308 """
309 309 entry = self._map.get(key)
310 310 if entry is not None:
311 311 return entry.state
312 312 return b'?'
313 313
314 314 def __contains__(self, key):
315 315 return key in self._map
316 316
317 317 def __iter__(self):
318 318 return iter(sorted(self._map))
319 319
320 320 def items(self):
321 321 return pycompat.iteritems(self._map)
322 322
323 323 iteritems = items
324 324
325 325 def directories(self):
326 326 return self._map.directories()
327 327
328 328 def parents(self):
329 329 return [self._validate(p) for p in self._pl]
330 330
331 331 def p1(self):
332 332 return self._validate(self._pl[0])
333 333
334 334 def p2(self):
335 335 return self._validate(self._pl[1])
336 336
337 337 @property
338 338 def in_merge(self):
339 339 """True if a merge is in progress"""
340 340 return self._pl[1] != self._nodeconstants.nullid
341 341
342 342 def branch(self):
343 343 return encoding.tolocal(self._branch)
344 344
345 345 def setparents(self, p1, p2=None):
346 346 """Set dirstate parents to p1 and p2.
347 347
348 348 When moving from two parents to one, "merged" entries a
349 349 adjusted to normal and previous copy records discarded and
350 350 returned by the call.
351 351
352 352 See localrepo.setparents()
353 353 """
354 354 if p2 is None:
355 355 p2 = self._nodeconstants.nullid
356 356 if self._parentwriters == 0:
357 357 raise ValueError(
358 358 b"cannot set dirstate parent outside of "
359 359 b"dirstate.parentchange context manager"
360 360 )
361 361
362 362 self._dirty = True
363 363 oldp2 = self._pl[1]
364 364 if self._origpl is None:
365 365 self._origpl = self._pl
366 366 self._map.setparents(p1, p2)
367 367 copies = {}
368 368 if (
369 369 oldp2 != self._nodeconstants.nullid
370 370 and p2 == self._nodeconstants.nullid
371 371 ):
372 372 candidatefiles = self._map.non_normal_or_other_parent_paths()
373 373
374 374 for f in candidatefiles:
375 375 s = self._map.get(f)
376 376 if s is None:
377 377 continue
378 378
379 379 # Discard "merged" markers when moving away from a merge state
380 380 if s.merged:
381 381 source = self._map.copymap.get(f)
382 382 if source:
383 383 copies[f] = source
384 384 self.normallookup(f)
385 385 # Also fix up otherparent markers
386 386 elif s.from_p2:
387 387 source = self._map.copymap.get(f)
388 388 if source:
389 389 copies[f] = source
390 390 self.add(f)
391 391 return copies
392 392
393 393 def setbranch(self, branch):
394 394 self.__class__._branch.set(self, encoding.fromlocal(branch))
395 395 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
396 396 try:
397 397 f.write(self._branch + b'\n')
398 398 f.close()
399 399
400 400 # make sure filecache has the correct stat info for _branch after
401 401 # replacing the underlying file
402 402 ce = self._filecache[b'_branch']
403 403 if ce:
404 404 ce.refresh()
405 405 except: # re-raises
406 406 f.discard()
407 407 raise
408 408
409 409 def invalidate(self):
410 410 """Causes the next access to reread the dirstate.
411 411
412 412 This is different from localrepo.invalidatedirstate() because it always
413 413 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
414 414 check whether the dirstate has changed before rereading it."""
415 415
416 416 for a in ("_map", "_branch", "_ignore"):
417 417 if a in self.__dict__:
418 418 delattr(self, a)
419 419 self._lastnormaltime = 0
420 420 self._dirty = False
421 421 self._updatedfiles.clear()
422 422 self._parentwriters = 0
423 423 self._origpl = None
424 424
425 425 def copy(self, source, dest):
426 426 """Mark dest as a copy of source. Unmark dest if source is None."""
427 427 if source == dest:
428 428 return
429 429 self._dirty = True
430 430 if source is not None:
431 431 self._map.copymap[dest] = source
432 432 self._updatedfiles.add(source)
433 433 self._updatedfiles.add(dest)
434 434 elif self._map.copymap.pop(dest, None):
435 435 self._updatedfiles.add(dest)
436 436
437 437 def copied(self, file):
438 438 return self._map.copymap.get(file, None)
439 439
440 440 def copies(self):
441 441 return self._map.copymap
442 442
443 443 def _addpath(
444 444 self,
445 445 f,
446 446 state=None,
447 447 mode=0,
448 448 size=None,
449 449 mtime=None,
450 450 added=False,
451 451 merged=False,
452 452 from_p2=False,
453 453 possibly_dirty=False,
454 454 ):
455 455 entry = self._map.get(f)
456 456 if added or entry is not None and entry.removed:
457 457 scmutil.checkfilename(f)
458 458 if self._map.hastrackeddir(f):
459 459 msg = _(b'directory %r already in dirstate')
460 460 msg %= pycompat.bytestr(f)
461 461 raise error.Abort(msg)
462 462 # shadows
463 463 for d in pathutil.finddirs(f):
464 464 if self._map.hastrackeddir(d):
465 465 break
466 466 entry = self._map.get(d)
467 467 if entry is not None and not entry.removed:
468 468 msg = _(b'file %r in dirstate clashes with %r')
469 469 msg %= (pycompat.bytestr(d), pycompat.bytestr(f))
470 470 raise error.Abort(msg)
471 471 self._dirty = True
472 472 self._updatedfiles.add(f)
473 473 self._map.addfile(
474 474 f,
475 475 state=state,
476 476 mode=mode,
477 477 size=size,
478 478 mtime=mtime,
479 479 added=added,
480 480 merged=merged,
481 481 from_p2=from_p2,
482 482 possibly_dirty=possibly_dirty,
483 483 )
484 484
485 485 def normal(self, f, parentfiledata=None):
486 486 """Mark a file normal and clean.
487 487
488 488 parentfiledata: (mode, size, mtime) of the clean file
489 489
490 490 parentfiledata should be computed from memory (for mode,
491 491 size), as or close as possible from the point where we
492 492 determined the file was clean, to limit the risk of the
493 493 file having been changed by an external process between the
494 494 moment where the file was determined to be clean and now."""
495 495 if parentfiledata:
496 496 (mode, size, mtime) = parentfiledata
497 497 else:
498 498 s = os.lstat(self._join(f))
499 499 mode = s.st_mode
500 500 size = s.st_size
501 501 mtime = s[stat.ST_MTIME]
502 502 self._addpath(f, b'n', mode, size, mtime)
503 503 self._map.copymap.pop(f, None)
504 504 if f in self._map.nonnormalset:
505 505 self._map.nonnormalset.remove(f)
506 506 if mtime > self._lastnormaltime:
507 507 # Remember the most recent modification timeslot for status(),
508 508 # to make sure we won't miss future size-preserving file content
509 509 # modifications that happen within the same timeslot.
510 510 self._lastnormaltime = mtime
511 511
512 512 def normallookup(self, f):
513 513 '''Mark a file normal, but possibly dirty.'''
514 514 if self.in_merge:
515 515 # if there is a merge going on and the file was either
516 516 # "merged" or coming from other parent (-2) before
517 517 # being removed, restore that state.
518 518 entry = self._map.get(f)
519 519 if entry is not None:
520 520 # XXX this should probably be dealt with a a lower level
521 521 # (see `merged_removed` and `from_p2_removed`)
522 522 if entry.merged_removed or entry.from_p2_removed:
523 523 source = self._map.copymap.get(f)
524 524 if entry.merged_removed:
525 525 self.merge(f)
526 526 elif entry.from_p2_removed:
527 527 self.otherparent(f)
528 528 if source is not None:
529 529 self.copy(source, f)
530 530 return
531 531 elif entry.merged or entry.from_p2:
532 532 return
533 533 self._addpath(f, possibly_dirty=True)
534 534 self._map.copymap.pop(f, None)
535 535
536 536 def otherparent(self, f):
537 537 '''Mark as coming from the other parent, always dirty.'''
538 538 if not self.in_merge:
539 539 msg = _(b"setting %r to other parent only allowed in merges") % f
540 540 raise error.Abort(msg)
541 541 if f in self and self[f] == b'n':
542 542 # merge-like
543 543 self._addpath(f, merged=True)
544 544 else:
545 545 # add-like
546 self._addpath(f, b'n', 0, from_p2=True)
546 self._addpath(f, from_p2=True)
547 547 self._map.copymap.pop(f, None)
548 548
549 549 def add(self, f):
550 550 '''Mark a file added.'''
551 551 self._addpath(f, added=True)
552 552 self._map.copymap.pop(f, None)
553 553
554 554 def remove(self, f):
555 555 '''Mark a file removed.'''
556 556 self._dirty = True
557 557 self._updatedfiles.add(f)
558 558 self._map.removefile(f, in_merge=self.in_merge)
559 559
560 560 def merge(self, f):
561 561 '''Mark a file merged.'''
562 562 if not self.in_merge:
563 563 return self.normallookup(f)
564 564 return self.otherparent(f)
565 565
566 566 def drop(self, f):
567 567 '''Drop a file from the dirstate'''
568 568 oldstate = self[f]
569 569 if self._map.dropfile(f, oldstate):
570 570 self._dirty = True
571 571 self._updatedfiles.add(f)
572 572 self._map.copymap.pop(f, None)
573 573
574 574 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
575 575 if exists is None:
576 576 exists = os.path.lexists(os.path.join(self._root, path))
577 577 if not exists:
578 578 # Maybe a path component exists
579 579 if not ignoremissing and b'/' in path:
580 580 d, f = path.rsplit(b'/', 1)
581 581 d = self._normalize(d, False, ignoremissing, None)
582 582 folded = d + b"/" + f
583 583 else:
584 584 # No path components, preserve original case
585 585 folded = path
586 586 else:
587 587 # recursively normalize leading directory components
588 588 # against dirstate
589 589 if b'/' in normed:
590 590 d, f = normed.rsplit(b'/', 1)
591 591 d = self._normalize(d, False, ignoremissing, True)
592 592 r = self._root + b"/" + d
593 593 folded = d + b"/" + util.fspath(f, r)
594 594 else:
595 595 folded = util.fspath(normed, self._root)
596 596 storemap[normed] = folded
597 597
598 598 return folded
599 599
600 600 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
601 601 normed = util.normcase(path)
602 602 folded = self._map.filefoldmap.get(normed, None)
603 603 if folded is None:
604 604 if isknown:
605 605 folded = path
606 606 else:
607 607 folded = self._discoverpath(
608 608 path, normed, ignoremissing, exists, self._map.filefoldmap
609 609 )
610 610 return folded
611 611
612 612 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
613 613 normed = util.normcase(path)
614 614 folded = self._map.filefoldmap.get(normed, None)
615 615 if folded is None:
616 616 folded = self._map.dirfoldmap.get(normed, None)
617 617 if folded is None:
618 618 if isknown:
619 619 folded = path
620 620 else:
621 621 # store discovered result in dirfoldmap so that future
622 622 # normalizefile calls don't start matching directories
623 623 folded = self._discoverpath(
624 624 path, normed, ignoremissing, exists, self._map.dirfoldmap
625 625 )
626 626 return folded
627 627
628 628 def normalize(self, path, isknown=False, ignoremissing=False):
629 629 """
630 630 normalize the case of a pathname when on a casefolding filesystem
631 631
632 632 isknown specifies whether the filename came from walking the
633 633 disk, to avoid extra filesystem access.
634 634
635 635 If ignoremissing is True, missing path are returned
636 636 unchanged. Otherwise, we try harder to normalize possibly
637 637 existing path components.
638 638
639 639 The normalized case is determined based on the following precedence:
640 640
641 641 - version of name already stored in the dirstate
642 642 - version of name stored on disk
643 643 - version provided via command arguments
644 644 """
645 645
646 646 if self._checkcase:
647 647 return self._normalize(path, isknown, ignoremissing)
648 648 return path
649 649
650 650 def clear(self):
651 651 self._map.clear()
652 652 self._lastnormaltime = 0
653 653 self._updatedfiles.clear()
654 654 self._dirty = True
655 655
656 656 def rebuild(self, parent, allfiles, changedfiles=None):
657 657 if changedfiles is None:
658 658 # Rebuild entire dirstate
659 659 to_lookup = allfiles
660 660 to_drop = []
661 661 lastnormaltime = self._lastnormaltime
662 662 self.clear()
663 663 self._lastnormaltime = lastnormaltime
664 664 elif len(changedfiles) < 10:
665 665 # Avoid turning allfiles into a set, which can be expensive if it's
666 666 # large.
667 667 to_lookup = []
668 668 to_drop = []
669 669 for f in changedfiles:
670 670 if f in allfiles:
671 671 to_lookup.append(f)
672 672 else:
673 673 to_drop.append(f)
674 674 else:
675 675 changedfilesset = set(changedfiles)
676 676 to_lookup = changedfilesset & set(allfiles)
677 677 to_drop = changedfilesset - to_lookup
678 678
679 679 if self._origpl is None:
680 680 self._origpl = self._pl
681 681 self._map.setparents(parent, self._nodeconstants.nullid)
682 682
683 683 for f in to_lookup:
684 684 self.normallookup(f)
685 685 for f in to_drop:
686 686 self.drop(f)
687 687
688 688 self._dirty = True
689 689
690 690 def identity(self):
691 691 """Return identity of dirstate itself to detect changing in storage
692 692
693 693 If identity of previous dirstate is equal to this, writing
694 694 changes based on the former dirstate out can keep consistency.
695 695 """
696 696 return self._map.identity
697 697
698 698 def write(self, tr):
699 699 if not self._dirty:
700 700 return
701 701
702 702 filename = self._filename
703 703 if tr:
704 704 # 'dirstate.write()' is not only for writing in-memory
705 705 # changes out, but also for dropping ambiguous timestamp.
706 706 # delayed writing re-raise "ambiguous timestamp issue".
707 707 # See also the wiki page below for detail:
708 708 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
709 709
710 710 # emulate dropping timestamp in 'parsers.pack_dirstate'
711 711 now = _getfsnow(self._opener)
712 712 self._map.clearambiguoustimes(self._updatedfiles, now)
713 713
714 714 # emulate that all 'dirstate.normal' results are written out
715 715 self._lastnormaltime = 0
716 716 self._updatedfiles.clear()
717 717
718 718 # delay writing in-memory changes out
719 719 tr.addfilegenerator(
720 720 b'dirstate',
721 721 (self._filename,),
722 722 self._writedirstate,
723 723 location=b'plain',
724 724 )
725 725 return
726 726
727 727 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
728 728 self._writedirstate(st)
729 729
730 730 def addparentchangecallback(self, category, callback):
731 731 """add a callback to be called when the wd parents are changed
732 732
733 733 Callback will be called with the following arguments:
734 734 dirstate, (oldp1, oldp2), (newp1, newp2)
735 735
736 736 Category is a unique identifier to allow overwriting an old callback
737 737 with a newer callback.
738 738 """
739 739 self._plchangecallbacks[category] = callback
740 740
741 741 def _writedirstate(self, st):
742 742 # notify callbacks about parents change
743 743 if self._origpl is not None and self._origpl != self._pl:
744 744 for c, callback in sorted(
745 745 pycompat.iteritems(self._plchangecallbacks)
746 746 ):
747 747 callback(self, self._origpl, self._pl)
748 748 self._origpl = None
749 749 # use the modification time of the newly created temporary file as the
750 750 # filesystem's notion of 'now'
751 751 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
752 752
753 753 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
754 754 # timestamp of each entries in dirstate, because of 'now > mtime'
755 755 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
756 756 if delaywrite > 0:
757 757 # do we have any files to delay for?
758 758 for f, e in pycompat.iteritems(self._map):
759 759 if e.state == b'n' and e[3] == now:
760 760 import time # to avoid useless import
761 761
762 762 # rather than sleep n seconds, sleep until the next
763 763 # multiple of n seconds
764 764 clock = time.time()
765 765 start = int(clock) - (int(clock) % delaywrite)
766 766 end = start + delaywrite
767 767 time.sleep(end - clock)
768 768 now = end # trust our estimate that the end is near now
769 769 break
770 770
771 771 self._map.write(st, now)
772 772 self._lastnormaltime = 0
773 773 self._dirty = False
774 774
775 775 def _dirignore(self, f):
776 776 if self._ignore(f):
777 777 return True
778 778 for p in pathutil.finddirs(f):
779 779 if self._ignore(p):
780 780 return True
781 781 return False
782 782
783 783 def _ignorefiles(self):
784 784 files = []
785 785 if os.path.exists(self._join(b'.hgignore')):
786 786 files.append(self._join(b'.hgignore'))
787 787 for name, path in self._ui.configitems(b"ui"):
788 788 if name == b'ignore' or name.startswith(b'ignore.'):
789 789 # we need to use os.path.join here rather than self._join
790 790 # because path is arbitrary and user-specified
791 791 files.append(os.path.join(self._rootdir, util.expandpath(path)))
792 792 return files
793 793
794 794 def _ignorefileandline(self, f):
795 795 files = collections.deque(self._ignorefiles())
796 796 visited = set()
797 797 while files:
798 798 i = files.popleft()
799 799 patterns = matchmod.readpatternfile(
800 800 i, self._ui.warn, sourceinfo=True
801 801 )
802 802 for pattern, lineno, line in patterns:
803 803 kind, p = matchmod._patsplit(pattern, b'glob')
804 804 if kind == b"subinclude":
805 805 if p not in visited:
806 806 files.append(p)
807 807 continue
808 808 m = matchmod.match(
809 809 self._root, b'', [], [pattern], warn=self._ui.warn
810 810 )
811 811 if m(f):
812 812 return (i, lineno, line)
813 813 visited.add(i)
814 814 return (None, -1, b"")
815 815
816 816 def _walkexplicit(self, match, subrepos):
817 817 """Get stat data about the files explicitly specified by match.
818 818
819 819 Return a triple (results, dirsfound, dirsnotfound).
820 820 - results is a mapping from filename to stat result. It also contains
821 821 listings mapping subrepos and .hg to None.
822 822 - dirsfound is a list of files found to be directories.
823 823 - dirsnotfound is a list of files that the dirstate thinks are
824 824 directories and that were not found."""
825 825
826 826 def badtype(mode):
827 827 kind = _(b'unknown')
828 828 if stat.S_ISCHR(mode):
829 829 kind = _(b'character device')
830 830 elif stat.S_ISBLK(mode):
831 831 kind = _(b'block device')
832 832 elif stat.S_ISFIFO(mode):
833 833 kind = _(b'fifo')
834 834 elif stat.S_ISSOCK(mode):
835 835 kind = _(b'socket')
836 836 elif stat.S_ISDIR(mode):
837 837 kind = _(b'directory')
838 838 return _(b'unsupported file type (type is %s)') % kind
839 839
840 840 badfn = match.bad
841 841 dmap = self._map
842 842 lstat = os.lstat
843 843 getkind = stat.S_IFMT
844 844 dirkind = stat.S_IFDIR
845 845 regkind = stat.S_IFREG
846 846 lnkkind = stat.S_IFLNK
847 847 join = self._join
848 848 dirsfound = []
849 849 foundadd = dirsfound.append
850 850 dirsnotfound = []
851 851 notfoundadd = dirsnotfound.append
852 852
853 853 if not match.isexact() and self._checkcase:
854 854 normalize = self._normalize
855 855 else:
856 856 normalize = None
857 857
858 858 files = sorted(match.files())
859 859 subrepos.sort()
860 860 i, j = 0, 0
861 861 while i < len(files) and j < len(subrepos):
862 862 subpath = subrepos[j] + b"/"
863 863 if files[i] < subpath:
864 864 i += 1
865 865 continue
866 866 while i < len(files) and files[i].startswith(subpath):
867 867 del files[i]
868 868 j += 1
869 869
870 870 if not files or b'' in files:
871 871 files = [b'']
872 872 # constructing the foldmap is expensive, so don't do it for the
873 873 # common case where files is ['']
874 874 normalize = None
875 875 results = dict.fromkeys(subrepos)
876 876 results[b'.hg'] = None
877 877
878 878 for ff in files:
879 879 if normalize:
880 880 nf = normalize(ff, False, True)
881 881 else:
882 882 nf = ff
883 883 if nf in results:
884 884 continue
885 885
886 886 try:
887 887 st = lstat(join(nf))
888 888 kind = getkind(st.st_mode)
889 889 if kind == dirkind:
890 890 if nf in dmap:
891 891 # file replaced by dir on disk but still in dirstate
892 892 results[nf] = None
893 893 foundadd((nf, ff))
894 894 elif kind == regkind or kind == lnkkind:
895 895 results[nf] = st
896 896 else:
897 897 badfn(ff, badtype(kind))
898 898 if nf in dmap:
899 899 results[nf] = None
900 900 except OSError as inst: # nf not found on disk - it is dirstate only
901 901 if nf in dmap: # does it exactly match a missing file?
902 902 results[nf] = None
903 903 else: # does it match a missing directory?
904 904 if self._map.hasdir(nf):
905 905 notfoundadd(nf)
906 906 else:
907 907 badfn(ff, encoding.strtolocal(inst.strerror))
908 908
909 909 # match.files() may contain explicitly-specified paths that shouldn't
910 910 # be taken; drop them from the list of files found. dirsfound/notfound
911 911 # aren't filtered here because they will be tested later.
912 912 if match.anypats():
913 913 for f in list(results):
914 914 if f == b'.hg' or f in subrepos:
915 915 # keep sentinel to disable further out-of-repo walks
916 916 continue
917 917 if not match(f):
918 918 del results[f]
919 919
920 920 # Case insensitive filesystems cannot rely on lstat() failing to detect
921 921 # a case-only rename. Prune the stat object for any file that does not
922 922 # match the case in the filesystem, if there are multiple files that
923 923 # normalize to the same path.
924 924 if match.isexact() and self._checkcase:
925 925 normed = {}
926 926
927 927 for f, st in pycompat.iteritems(results):
928 928 if st is None:
929 929 continue
930 930
931 931 nc = util.normcase(f)
932 932 paths = normed.get(nc)
933 933
934 934 if paths is None:
935 935 paths = set()
936 936 normed[nc] = paths
937 937
938 938 paths.add(f)
939 939
940 940 for norm, paths in pycompat.iteritems(normed):
941 941 if len(paths) > 1:
942 942 for path in paths:
943 943 folded = self._discoverpath(
944 944 path, norm, True, None, self._map.dirfoldmap
945 945 )
946 946 if path != folded:
947 947 results[path] = None
948 948
949 949 return results, dirsfound, dirsnotfound
950 950
951 951 def walk(self, match, subrepos, unknown, ignored, full=True):
952 952 """
953 953 Walk recursively through the directory tree, finding all files
954 954 matched by match.
955 955
956 956 If full is False, maybe skip some known-clean files.
957 957
958 958 Return a dict mapping filename to stat-like object (either
959 959 mercurial.osutil.stat instance or return value of os.stat()).
960 960
961 961 """
962 962 # full is a flag that extensions that hook into walk can use -- this
963 963 # implementation doesn't use it at all. This satisfies the contract
964 964 # because we only guarantee a "maybe".
965 965
966 966 if ignored:
967 967 ignore = util.never
968 968 dirignore = util.never
969 969 elif unknown:
970 970 ignore = self._ignore
971 971 dirignore = self._dirignore
972 972 else:
973 973 # if not unknown and not ignored, drop dir recursion and step 2
974 974 ignore = util.always
975 975 dirignore = util.always
976 976
977 977 matchfn = match.matchfn
978 978 matchalways = match.always()
979 979 matchtdir = match.traversedir
980 980 dmap = self._map
981 981 listdir = util.listdir
982 982 lstat = os.lstat
983 983 dirkind = stat.S_IFDIR
984 984 regkind = stat.S_IFREG
985 985 lnkkind = stat.S_IFLNK
986 986 join = self._join
987 987
988 988 exact = skipstep3 = False
989 989 if match.isexact(): # match.exact
990 990 exact = True
991 991 dirignore = util.always # skip step 2
992 992 elif match.prefix(): # match.match, no patterns
993 993 skipstep3 = True
994 994
995 995 if not exact and self._checkcase:
996 996 normalize = self._normalize
997 997 normalizefile = self._normalizefile
998 998 skipstep3 = False
999 999 else:
1000 1000 normalize = self._normalize
1001 1001 normalizefile = None
1002 1002
1003 1003 # step 1: find all explicit files
1004 1004 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1005 1005 if matchtdir:
1006 1006 for d in work:
1007 1007 matchtdir(d[0])
1008 1008 for d in dirsnotfound:
1009 1009 matchtdir(d)
1010 1010
1011 1011 skipstep3 = skipstep3 and not (work or dirsnotfound)
1012 1012 work = [d for d in work if not dirignore(d[0])]
1013 1013
1014 1014 # step 2: visit subdirectories
1015 1015 def traverse(work, alreadynormed):
1016 1016 wadd = work.append
1017 1017 while work:
1018 1018 tracing.counter('dirstate.walk work', len(work))
1019 1019 nd = work.pop()
1020 1020 visitentries = match.visitchildrenset(nd)
1021 1021 if not visitentries:
1022 1022 continue
1023 1023 if visitentries == b'this' or visitentries == b'all':
1024 1024 visitentries = None
1025 1025 skip = None
1026 1026 if nd != b'':
1027 1027 skip = b'.hg'
1028 1028 try:
1029 1029 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1030 1030 entries = listdir(join(nd), stat=True, skip=skip)
1031 1031 except OSError as inst:
1032 1032 if inst.errno in (errno.EACCES, errno.ENOENT):
1033 1033 match.bad(
1034 1034 self.pathto(nd), encoding.strtolocal(inst.strerror)
1035 1035 )
1036 1036 continue
1037 1037 raise
1038 1038 for f, kind, st in entries:
1039 1039 # Some matchers may return files in the visitentries set,
1040 1040 # instead of 'this', if the matcher explicitly mentions them
1041 1041 # and is not an exactmatcher. This is acceptable; we do not
1042 1042 # make any hard assumptions about file-or-directory below
1043 1043 # based on the presence of `f` in visitentries. If
1044 1044 # visitchildrenset returned a set, we can always skip the
1045 1045 # entries *not* in the set it provided regardless of whether
1046 1046 # they're actually a file or a directory.
1047 1047 if visitentries and f not in visitentries:
1048 1048 continue
1049 1049 if normalizefile:
1050 1050 # even though f might be a directory, we're only
1051 1051 # interested in comparing it to files currently in the
1052 1052 # dmap -- therefore normalizefile is enough
1053 1053 nf = normalizefile(
1054 1054 nd and (nd + b"/" + f) or f, True, True
1055 1055 )
1056 1056 else:
1057 1057 nf = nd and (nd + b"/" + f) or f
1058 1058 if nf not in results:
1059 1059 if kind == dirkind:
1060 1060 if not ignore(nf):
1061 1061 if matchtdir:
1062 1062 matchtdir(nf)
1063 1063 wadd(nf)
1064 1064 if nf in dmap and (matchalways or matchfn(nf)):
1065 1065 results[nf] = None
1066 1066 elif kind == regkind or kind == lnkkind:
1067 1067 if nf in dmap:
1068 1068 if matchalways or matchfn(nf):
1069 1069 results[nf] = st
1070 1070 elif (matchalways or matchfn(nf)) and not ignore(
1071 1071 nf
1072 1072 ):
1073 1073 # unknown file -- normalize if necessary
1074 1074 if not alreadynormed:
1075 1075 nf = normalize(nf, False, True)
1076 1076 results[nf] = st
1077 1077 elif nf in dmap and (matchalways or matchfn(nf)):
1078 1078 results[nf] = None
1079 1079
1080 1080 for nd, d in work:
1081 1081 # alreadynormed means that processwork doesn't have to do any
1082 1082 # expensive directory normalization
1083 1083 alreadynormed = not normalize or nd == d
1084 1084 traverse([d], alreadynormed)
1085 1085
1086 1086 for s in subrepos:
1087 1087 del results[s]
1088 1088 del results[b'.hg']
1089 1089
1090 1090 # step 3: visit remaining files from dmap
1091 1091 if not skipstep3 and not exact:
1092 1092 # If a dmap file is not in results yet, it was either
1093 1093 # a) not matching matchfn b) ignored, c) missing, or d) under a
1094 1094 # symlink directory.
1095 1095 if not results and matchalways:
1096 1096 visit = [f for f in dmap]
1097 1097 else:
1098 1098 visit = [f for f in dmap if f not in results and matchfn(f)]
1099 1099 visit.sort()
1100 1100
1101 1101 if unknown:
1102 1102 # unknown == True means we walked all dirs under the roots
1103 1103 # that wasn't ignored, and everything that matched was stat'ed
1104 1104 # and is already in results.
1105 1105 # The rest must thus be ignored or under a symlink.
1106 1106 audit_path = pathutil.pathauditor(self._root, cached=True)
1107 1107
1108 1108 for nf in iter(visit):
1109 1109 # If a stat for the same file was already added with a
1110 1110 # different case, don't add one for this, since that would
1111 1111 # make it appear as if the file exists under both names
1112 1112 # on disk.
1113 1113 if (
1114 1114 normalizefile
1115 1115 and normalizefile(nf, True, True) in results
1116 1116 ):
1117 1117 results[nf] = None
1118 1118 # Report ignored items in the dmap as long as they are not
1119 1119 # under a symlink directory.
1120 1120 elif audit_path.check(nf):
1121 1121 try:
1122 1122 results[nf] = lstat(join(nf))
1123 1123 # file was just ignored, no links, and exists
1124 1124 except OSError:
1125 1125 # file doesn't exist
1126 1126 results[nf] = None
1127 1127 else:
1128 1128 # It's either missing or under a symlink directory
1129 1129 # which we in this case report as missing
1130 1130 results[nf] = None
1131 1131 else:
1132 1132 # We may not have walked the full directory tree above,
1133 1133 # so stat and check everything we missed.
1134 1134 iv = iter(visit)
1135 1135 for st in util.statfiles([join(i) for i in visit]):
1136 1136 results[next(iv)] = st
1137 1137 return results
1138 1138
1139 1139 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1140 1140 # Force Rayon (Rust parallelism library) to respect the number of
1141 1141 # workers. This is a temporary workaround until Rust code knows
1142 1142 # how to read the config file.
1143 1143 numcpus = self._ui.configint(b"worker", b"numcpus")
1144 1144 if numcpus is not None:
1145 1145 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1146 1146
1147 1147 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1148 1148 if not workers_enabled:
1149 1149 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1150 1150
1151 1151 (
1152 1152 lookup,
1153 1153 modified,
1154 1154 added,
1155 1155 removed,
1156 1156 deleted,
1157 1157 clean,
1158 1158 ignored,
1159 1159 unknown,
1160 1160 warnings,
1161 1161 bad,
1162 1162 traversed,
1163 1163 dirty,
1164 1164 ) = rustmod.status(
1165 1165 self._map._rustmap,
1166 1166 matcher,
1167 1167 self._rootdir,
1168 1168 self._ignorefiles(),
1169 1169 self._checkexec,
1170 1170 self._lastnormaltime,
1171 1171 bool(list_clean),
1172 1172 bool(list_ignored),
1173 1173 bool(list_unknown),
1174 1174 bool(matcher.traversedir),
1175 1175 )
1176 1176
1177 1177 self._dirty |= dirty
1178 1178
1179 1179 if matcher.traversedir:
1180 1180 for dir in traversed:
1181 1181 matcher.traversedir(dir)
1182 1182
1183 1183 if self._ui.warn:
1184 1184 for item in warnings:
1185 1185 if isinstance(item, tuple):
1186 1186 file_path, syntax = item
1187 1187 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1188 1188 file_path,
1189 1189 syntax,
1190 1190 )
1191 1191 self._ui.warn(msg)
1192 1192 else:
1193 1193 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1194 1194 self._ui.warn(
1195 1195 msg
1196 1196 % (
1197 1197 pathutil.canonpath(
1198 1198 self._rootdir, self._rootdir, item
1199 1199 ),
1200 1200 b"No such file or directory",
1201 1201 )
1202 1202 )
1203 1203
1204 1204 for (fn, message) in bad:
1205 1205 matcher.bad(fn, encoding.strtolocal(message))
1206 1206
1207 1207 status = scmutil.status(
1208 1208 modified=modified,
1209 1209 added=added,
1210 1210 removed=removed,
1211 1211 deleted=deleted,
1212 1212 unknown=unknown,
1213 1213 ignored=ignored,
1214 1214 clean=clean,
1215 1215 )
1216 1216 return (lookup, status)
1217 1217
1218 1218 def status(self, match, subrepos, ignored, clean, unknown):
1219 1219 """Determine the status of the working copy relative to the
1220 1220 dirstate and return a pair of (unsure, status), where status is of type
1221 1221 scmutil.status and:
1222 1222
1223 1223 unsure:
1224 1224 files that might have been modified since the dirstate was
1225 1225 written, but need to be read to be sure (size is the same
1226 1226 but mtime differs)
1227 1227 status.modified:
1228 1228 files that have definitely been modified since the dirstate
1229 1229 was written (different size or mode)
1230 1230 status.clean:
1231 1231 files that have definitely not been modified since the
1232 1232 dirstate was written
1233 1233 """
1234 1234 listignored, listclean, listunknown = ignored, clean, unknown
1235 1235 lookup, modified, added, unknown, ignored = [], [], [], [], []
1236 1236 removed, deleted, clean = [], [], []
1237 1237
1238 1238 dmap = self._map
1239 1239 dmap.preload()
1240 1240
1241 1241 use_rust = True
1242 1242
1243 1243 allowed_matchers = (
1244 1244 matchmod.alwaysmatcher,
1245 1245 matchmod.exactmatcher,
1246 1246 matchmod.includematcher,
1247 1247 )
1248 1248
1249 1249 if rustmod is None:
1250 1250 use_rust = False
1251 1251 elif self._checkcase:
1252 1252 # Case-insensitive filesystems are not handled yet
1253 1253 use_rust = False
1254 1254 elif subrepos:
1255 1255 use_rust = False
1256 1256 elif sparse.enabled:
1257 1257 use_rust = False
1258 1258 elif not isinstance(match, allowed_matchers):
1259 1259 # Some matchers have yet to be implemented
1260 1260 use_rust = False
1261 1261
1262 1262 if use_rust:
1263 1263 try:
1264 1264 return self._rust_status(
1265 1265 match, listclean, listignored, listunknown
1266 1266 )
1267 1267 except rustmod.FallbackError:
1268 1268 pass
1269 1269
1270 1270 def noop(f):
1271 1271 pass
1272 1272
1273 1273 dcontains = dmap.__contains__
1274 1274 dget = dmap.__getitem__
1275 1275 ladd = lookup.append # aka "unsure"
1276 1276 madd = modified.append
1277 1277 aadd = added.append
1278 1278 uadd = unknown.append if listunknown else noop
1279 1279 iadd = ignored.append if listignored else noop
1280 1280 radd = removed.append
1281 1281 dadd = deleted.append
1282 1282 cadd = clean.append if listclean else noop
1283 1283 mexact = match.exact
1284 1284 dirignore = self._dirignore
1285 1285 checkexec = self._checkexec
1286 1286 copymap = self._map.copymap
1287 1287 lastnormaltime = self._lastnormaltime
1288 1288
1289 1289 # We need to do full walks when either
1290 1290 # - we're listing all clean files, or
1291 1291 # - match.traversedir does something, because match.traversedir should
1292 1292 # be called for every dir in the working dir
1293 1293 full = listclean or match.traversedir is not None
1294 1294 for fn, st in pycompat.iteritems(
1295 1295 self.walk(match, subrepos, listunknown, listignored, full=full)
1296 1296 ):
1297 1297 if not dcontains(fn):
1298 1298 if (listignored or mexact(fn)) and dirignore(fn):
1299 1299 if listignored:
1300 1300 iadd(fn)
1301 1301 else:
1302 1302 uadd(fn)
1303 1303 continue
1304 1304
1305 1305 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1306 1306 # written like that for performance reasons. dmap[fn] is not a
1307 1307 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1308 1308 # opcode has fast paths when the value to be unpacked is a tuple or
1309 1309 # a list, but falls back to creating a full-fledged iterator in
1310 1310 # general. That is much slower than simply accessing and storing the
1311 1311 # tuple members one by one.
1312 1312 t = dget(fn)
1313 1313 state = t.state
1314 1314 mode = t[1]
1315 1315 size = t[2]
1316 1316 time = t[3]
1317 1317
1318 1318 if not st and state in b"nma":
1319 1319 dadd(fn)
1320 1320 elif state == b'n':
1321 1321 if (
1322 1322 size >= 0
1323 1323 and (
1324 1324 (size != st.st_size and size != st.st_size & _rangemask)
1325 1325 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1326 1326 )
1327 1327 or t.from_p2
1328 1328 or fn in copymap
1329 1329 ):
1330 1330 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1331 1331 # issue6456: Size returned may be longer due to
1332 1332 # encryption on EXT-4 fscrypt, undecided.
1333 1333 ladd(fn)
1334 1334 else:
1335 1335 madd(fn)
1336 1336 elif (
1337 1337 time != st[stat.ST_MTIME]
1338 1338 and time != st[stat.ST_MTIME] & _rangemask
1339 1339 ):
1340 1340 ladd(fn)
1341 1341 elif st[stat.ST_MTIME] == lastnormaltime:
1342 1342 # fn may have just been marked as normal and it may have
1343 1343 # changed in the same second without changing its size.
1344 1344 # This can happen if we quickly do multiple commits.
1345 1345 # Force lookup, so we don't miss such a racy file change.
1346 1346 ladd(fn)
1347 1347 elif listclean:
1348 1348 cadd(fn)
1349 1349 elif t.merged:
1350 1350 madd(fn)
1351 1351 elif t.added:
1352 1352 aadd(fn)
1353 1353 elif t.removed:
1354 1354 radd(fn)
1355 1355 status = scmutil.status(
1356 1356 modified, added, removed, deleted, unknown, ignored, clean
1357 1357 )
1358 1358 return (lookup, status)
1359 1359
1360 1360 def matches(self, match):
1361 1361 """
1362 1362 return files in the dirstate (in whatever state) filtered by match
1363 1363 """
1364 1364 dmap = self._map
1365 1365 if rustmod is not None:
1366 1366 dmap = self._map._rustmap
1367 1367
1368 1368 if match.always():
1369 1369 return dmap.keys()
1370 1370 files = match.files()
1371 1371 if match.isexact():
1372 1372 # fast path -- filter the other way around, since typically files is
1373 1373 # much smaller than dmap
1374 1374 return [f for f in files if f in dmap]
1375 1375 if match.prefix() and all(fn in dmap for fn in files):
1376 1376 # fast path -- all the values are known to be files, so just return
1377 1377 # that
1378 1378 return list(files)
1379 1379 return [f for f in dmap if match(f)]
1380 1380
1381 1381 def _actualfilename(self, tr):
1382 1382 if tr:
1383 1383 return self._pendingfilename
1384 1384 else:
1385 1385 return self._filename
1386 1386
1387 1387 def savebackup(self, tr, backupname):
1388 1388 '''Save current dirstate into backup file'''
1389 1389 filename = self._actualfilename(tr)
1390 1390 assert backupname != filename
1391 1391
1392 1392 # use '_writedirstate' instead of 'write' to write changes certainly,
1393 1393 # because the latter omits writing out if transaction is running.
1394 1394 # output file will be used to create backup of dirstate at this point.
1395 1395 if self._dirty or not self._opener.exists(filename):
1396 1396 self._writedirstate(
1397 1397 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1398 1398 )
1399 1399
1400 1400 if tr:
1401 1401 # ensure that subsequent tr.writepending returns True for
1402 1402 # changes written out above, even if dirstate is never
1403 1403 # changed after this
1404 1404 tr.addfilegenerator(
1405 1405 b'dirstate',
1406 1406 (self._filename,),
1407 1407 self._writedirstate,
1408 1408 location=b'plain',
1409 1409 )
1410 1410
1411 1411 # ensure that pending file written above is unlinked at
1412 1412 # failure, even if tr.writepending isn't invoked until the
1413 1413 # end of this transaction
1414 1414 tr.registertmp(filename, location=b'plain')
1415 1415
1416 1416 self._opener.tryunlink(backupname)
1417 1417 # hardlink backup is okay because _writedirstate is always called
1418 1418 # with an "atomictemp=True" file.
1419 1419 util.copyfile(
1420 1420 self._opener.join(filename),
1421 1421 self._opener.join(backupname),
1422 1422 hardlink=True,
1423 1423 )
1424 1424
1425 1425 def restorebackup(self, tr, backupname):
1426 1426 '''Restore dirstate by backup file'''
1427 1427 # this "invalidate()" prevents "wlock.release()" from writing
1428 1428 # changes of dirstate out after restoring from backup file
1429 1429 self.invalidate()
1430 1430 filename = self._actualfilename(tr)
1431 1431 o = self._opener
1432 1432 if util.samefile(o.join(backupname), o.join(filename)):
1433 1433 o.unlink(backupname)
1434 1434 else:
1435 1435 o.rename(backupname, filename, checkambig=True)
1436 1436
1437 1437 def clearbackup(self, tr, backupname):
1438 1438 '''Clear backup file'''
1439 1439 self._opener.unlink(backupname)
@@ -1,684 +1,685 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 parsers = policy.importmod('parsers')
22 22 rustmod = policy.importrust('dirstate')
23 23
24 24 propertycache = util.propertycache
25 25
26 26 dirstatetuple = parsers.dirstatetuple
27 27
28 28
29 29 # a special value used internally for `size` if the file come from the other parent
30 30 FROM_P2 = -2
31 31
32 32 # a special value used internally for `size` if the file is modified/merged/added
33 33 NONNORMAL = -1
34 34
35 35 # a special value used internally for `time` if the time is ambigeous
36 36 AMBIGUOUS_TIME = -1
37 37
38 38 rangemask = 0x7FFFFFFF
39 39
40 40
41 41 class dirstatemap(object):
42 42 """Map encapsulating the dirstate's contents.
43 43
44 44 The dirstate contains the following state:
45 45
46 46 - `identity` is the identity of the dirstate file, which can be used to
47 47 detect when changes have occurred to the dirstate file.
48 48
49 49 - `parents` is a pair containing the parents of the working copy. The
50 50 parents are updated by calling `setparents`.
51 51
52 52 - the state map maps filenames to tuples of (state, mode, size, mtime),
53 53 where state is a single character representing 'normal', 'added',
54 54 'removed', or 'merged'. It is read by treating the dirstate as a
55 55 dict. File state is updated by calling the `addfile`, `removefile` and
56 56 `dropfile` methods.
57 57
58 58 - `copymap` maps destination filenames to their source filename.
59 59
60 60 The dirstate also provides the following views onto the state:
61 61
62 62 - `nonnormalset` is a set of the filenames that have state other
63 63 than 'normal', or are normal but have an mtime of -1 ('normallookup').
64 64
65 65 - `otherparentset` is a set of the filenames that are marked as coming
66 66 from the second parent when the dirstate is currently being merged.
67 67
68 68 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
69 69 form that they appear as in the dirstate.
70 70
71 71 - `dirfoldmap` is a dict mapping normalized directory names to the
72 72 denormalized form that they appear as in the dirstate.
73 73 """
74 74
75 75 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
76 76 self._ui = ui
77 77 self._opener = opener
78 78 self._root = root
79 79 self._filename = b'dirstate'
80 80 self._nodelen = 20
81 81 self._nodeconstants = nodeconstants
82 82 assert (
83 83 not use_dirstate_v2
84 84 ), "should have detected unsupported requirement"
85 85
86 86 self._parents = None
87 87 self._dirtyparents = False
88 88
89 89 # for consistent view between _pl() and _read() invocations
90 90 self._pendingmode = None
91 91
92 92 @propertycache
93 93 def _map(self):
94 94 self._map = {}
95 95 self.read()
96 96 return self._map
97 97
98 98 @propertycache
99 99 def copymap(self):
100 100 self.copymap = {}
101 101 self._map
102 102 return self.copymap
103 103
104 104 def directories(self):
105 105 # Rust / dirstate-v2 only
106 106 return []
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 def __len__(self):
126 126 return len(self._map)
127 127
128 128 def __iter__(self):
129 129 return iter(self._map)
130 130
131 131 def get(self, key, default=None):
132 132 return self._map.get(key, default)
133 133
134 134 def __contains__(self, key):
135 135 return key in self._map
136 136
137 137 def __getitem__(self, key):
138 138 return self._map[key]
139 139
140 140 def keys(self):
141 141 return self._map.keys()
142 142
143 143 def preload(self):
144 144 """Loads the underlying data, if it's not already loaded"""
145 145 self._map
146 146
147 147 def addfile(
148 148 self,
149 149 f,
150 150 state=None,
151 151 mode=0,
152 152 size=None,
153 153 mtime=None,
154 154 added=False,
155 155 merged=False,
156 156 from_p2=False,
157 157 possibly_dirty=False,
158 158 ):
159 159 """Add a tracked file to the dirstate."""
160 160 if added:
161 161 assert not merged
162 162 assert not possibly_dirty
163 163 assert not from_p2
164 164 state = b'a'
165 165 size = NONNORMAL
166 166 mtime = AMBIGUOUS_TIME
167 167 elif merged:
168 168 assert not possibly_dirty
169 169 assert not from_p2
170 170 state = b'm'
171 171 size = FROM_P2
172 172 mtime = AMBIGUOUS_TIME
173 173 elif from_p2:
174 174 assert not possibly_dirty
175 state = b'n'
175 176 size = FROM_P2
176 177 mtime = AMBIGUOUS_TIME
177 178 elif possibly_dirty:
178 179 state = b'n'
179 180 size = NONNORMAL
180 181 mtime = AMBIGUOUS_TIME
181 182 else:
182 183 assert state != b'a'
183 184 assert size != FROM_P2
184 185 assert size != NONNORMAL
185 186 size = size & rangemask
186 187 mtime = mtime & rangemask
187 188 assert state is not None
188 189 assert size is not None
189 190 assert mtime is not None
190 191 old_entry = self.get(f)
191 192 if (
192 193 old_entry is None or old_entry.removed
193 194 ) and "_dirs" in self.__dict__:
194 195 self._dirs.addpath(f)
195 196 if old_entry is None and "_alldirs" in self.__dict__:
196 197 self._alldirs.addpath(f)
197 198 self._map[f] = dirstatetuple(state, mode, size, mtime)
198 199 if state != b'n' or mtime == AMBIGUOUS_TIME:
199 200 self.nonnormalset.add(f)
200 201 if size == FROM_P2:
201 202 self.otherparentset.add(f)
202 203
203 204 def removefile(self, f, in_merge=False):
204 205 """
205 206 Mark a file as removed in the dirstate.
206 207
207 208 The `size` parameter is used to store sentinel values that indicate
208 209 the file's previous state. In the future, we should refactor this
209 210 to be more explicit about what that state is.
210 211 """
211 212 entry = self.get(f)
212 213 size = 0
213 214 if in_merge:
214 215 # XXX we should not be able to have 'm' state and 'FROM_P2' if not
215 216 # during a merge. So I (marmoute) am not sure we need the
216 217 # conditionnal at all. Adding double checking this with assert
217 218 # would be nice.
218 219 if entry is not None:
219 220 # backup the previous state
220 221 if entry.merged: # merge
221 222 size = NONNORMAL
222 223 elif entry[0] == b'n' and entry.from_p2:
223 224 size = FROM_P2
224 225 self.otherparentset.add(f)
225 226 if size == 0:
226 227 self.copymap.pop(f, None)
227 228
228 229 if entry is not None and entry[0] != b'r' and "_dirs" in self.__dict__:
229 230 self._dirs.delpath(f)
230 231 if entry is None and "_alldirs" in self.__dict__:
231 232 self._alldirs.addpath(f)
232 233 if "filefoldmap" in self.__dict__:
233 234 normed = util.normcase(f)
234 235 self.filefoldmap.pop(normed, None)
235 236 self._map[f] = dirstatetuple(b'r', 0, size, 0)
236 237 self.nonnormalset.add(f)
237 238
238 239 def dropfile(self, f, oldstate):
239 240 """
240 241 Remove a file from the dirstate. Returns True if the file was
241 242 previously recorded.
242 243 """
243 244 exists = self._map.pop(f, None) is not None
244 245 if exists:
245 246 if oldstate != b"r" and "_dirs" in self.__dict__:
246 247 self._dirs.delpath(f)
247 248 if "_alldirs" in self.__dict__:
248 249 self._alldirs.delpath(f)
249 250 if "filefoldmap" in self.__dict__:
250 251 normed = util.normcase(f)
251 252 self.filefoldmap.pop(normed, None)
252 253 self.nonnormalset.discard(f)
253 254 return exists
254 255
255 256 def clearambiguoustimes(self, files, now):
256 257 for f in files:
257 258 e = self.get(f)
258 259 if e is not None and e[0] == b'n' and e[3] == now:
259 260 self._map[f] = dirstatetuple(e[0], e[1], e[2], AMBIGUOUS_TIME)
260 261 self.nonnormalset.add(f)
261 262
262 263 def nonnormalentries(self):
263 264 '''Compute the nonnormal dirstate entries from the dmap'''
264 265 try:
265 266 return parsers.nonnormalotherparententries(self._map)
266 267 except AttributeError:
267 268 nonnorm = set()
268 269 otherparent = set()
269 270 for fname, e in pycompat.iteritems(self._map):
270 271 if e[0] != b'n' or e[3] == AMBIGUOUS_TIME:
271 272 nonnorm.add(fname)
272 273 if e[0] == b'n' and e[2] == FROM_P2:
273 274 otherparent.add(fname)
274 275 return nonnorm, otherparent
275 276
276 277 @propertycache
277 278 def filefoldmap(self):
278 279 """Returns a dictionary mapping normalized case paths to their
279 280 non-normalized versions.
280 281 """
281 282 try:
282 283 makefilefoldmap = parsers.make_file_foldmap
283 284 except AttributeError:
284 285 pass
285 286 else:
286 287 return makefilefoldmap(
287 288 self._map, util.normcasespec, util.normcasefallback
288 289 )
289 290
290 291 f = {}
291 292 normcase = util.normcase
292 293 for name, s in pycompat.iteritems(self._map):
293 294 if s[0] != b'r':
294 295 f[normcase(name)] = name
295 296 f[b'.'] = b'.' # prevents useless util.fspath() invocation
296 297 return f
297 298
298 299 def hastrackeddir(self, d):
299 300 """
300 301 Returns True if the dirstate contains a tracked (not removed) file
301 302 in this directory.
302 303 """
303 304 return d in self._dirs
304 305
305 306 def hasdir(self, d):
306 307 """
307 308 Returns True if the dirstate contains a file (tracked or removed)
308 309 in this directory.
309 310 """
310 311 return d in self._alldirs
311 312
312 313 @propertycache
313 314 def _dirs(self):
314 315 return pathutil.dirs(self._map, b'r')
315 316
316 317 @propertycache
317 318 def _alldirs(self):
318 319 return pathutil.dirs(self._map)
319 320
320 321 def _opendirstatefile(self):
321 322 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
322 323 if self._pendingmode is not None and self._pendingmode != mode:
323 324 fp.close()
324 325 raise error.Abort(
325 326 _(b'working directory state may be changed parallelly')
326 327 )
327 328 self._pendingmode = mode
328 329 return fp
329 330
330 331 def parents(self):
331 332 if not self._parents:
332 333 try:
333 334 fp = self._opendirstatefile()
334 335 st = fp.read(2 * self._nodelen)
335 336 fp.close()
336 337 except IOError as err:
337 338 if err.errno != errno.ENOENT:
338 339 raise
339 340 # File doesn't exist, so the current state is empty
340 341 st = b''
341 342
342 343 l = len(st)
343 344 if l == self._nodelen * 2:
344 345 self._parents = (
345 346 st[: self._nodelen],
346 347 st[self._nodelen : 2 * self._nodelen],
347 348 )
348 349 elif l == 0:
349 350 self._parents = (
350 351 self._nodeconstants.nullid,
351 352 self._nodeconstants.nullid,
352 353 )
353 354 else:
354 355 raise error.Abort(
355 356 _(b'working directory state appears damaged!')
356 357 )
357 358
358 359 return self._parents
359 360
360 361 def setparents(self, p1, p2):
361 362 self._parents = (p1, p2)
362 363 self._dirtyparents = True
363 364
364 365 def read(self):
365 366 # ignore HG_PENDING because identity is used only for writing
366 367 self.identity = util.filestat.frompath(
367 368 self._opener.join(self._filename)
368 369 )
369 370
370 371 try:
371 372 fp = self._opendirstatefile()
372 373 try:
373 374 st = fp.read()
374 375 finally:
375 376 fp.close()
376 377 except IOError as err:
377 378 if err.errno != errno.ENOENT:
378 379 raise
379 380 return
380 381 if not st:
381 382 return
382 383
383 384 if util.safehasattr(parsers, b'dict_new_presized'):
384 385 # Make an estimate of the number of files in the dirstate based on
385 386 # its size. This trades wasting some memory for avoiding costly
386 387 # resizes. Each entry have a prefix of 17 bytes followed by one or
387 388 # two path names. Studies on various large-scale real-world repositories
388 389 # found 54 bytes a reasonable upper limit for the average path names.
389 390 # Copy entries are ignored for the sake of this estimate.
390 391 self._map = parsers.dict_new_presized(len(st) // 71)
391 392
392 393 # Python's garbage collector triggers a GC each time a certain number
393 394 # of container objects (the number being defined by
394 395 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
395 396 # for each file in the dirstate. The C version then immediately marks
396 397 # them as not to be tracked by the collector. However, this has no
397 398 # effect on when GCs are triggered, only on what objects the GC looks
398 399 # into. This means that O(number of files) GCs are unavoidable.
399 400 # Depending on when in the process's lifetime the dirstate is parsed,
400 401 # this can get very expensive. As a workaround, disable GC while
401 402 # parsing the dirstate.
402 403 #
403 404 # (we cannot decorate the function directly since it is in a C module)
404 405 parse_dirstate = util.nogc(parsers.parse_dirstate)
405 406 p = parse_dirstate(self._map, self.copymap, st)
406 407 if not self._dirtyparents:
407 408 self.setparents(*p)
408 409
409 410 # Avoid excess attribute lookups by fast pathing certain checks
410 411 self.__contains__ = self._map.__contains__
411 412 self.__getitem__ = self._map.__getitem__
412 413 self.get = self._map.get
413 414
414 415 def write(self, st, now):
415 416 st.write(
416 417 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
417 418 )
418 419 st.close()
419 420 self._dirtyparents = False
420 421 self.nonnormalset, self.otherparentset = self.nonnormalentries()
421 422
422 423 @propertycache
423 424 def nonnormalset(self):
424 425 nonnorm, otherparents = self.nonnormalentries()
425 426 self.otherparentset = otherparents
426 427 return nonnorm
427 428
428 429 @propertycache
429 430 def otherparentset(self):
430 431 nonnorm, otherparents = self.nonnormalentries()
431 432 self.nonnormalset = nonnorm
432 433 return otherparents
433 434
434 435 def non_normal_or_other_parent_paths(self):
435 436 return self.nonnormalset.union(self.otherparentset)
436 437
437 438 @propertycache
438 439 def identity(self):
439 440 self._map
440 441 return self.identity
441 442
442 443 @propertycache
443 444 def dirfoldmap(self):
444 445 f = {}
445 446 normcase = util.normcase
446 447 for name in self._dirs:
447 448 f[normcase(name)] = name
448 449 return f
449 450
450 451
451 452 if rustmod is not None:
452 453
453 454 class dirstatemap(object):
454 455 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
455 456 self._use_dirstate_v2 = use_dirstate_v2
456 457 self._nodeconstants = nodeconstants
457 458 self._ui = ui
458 459 self._opener = opener
459 460 self._root = root
460 461 self._filename = b'dirstate'
461 462 self._nodelen = 20 # Also update Rust code when changing this!
462 463 self._parents = None
463 464 self._dirtyparents = False
464 465
465 466 # for consistent view between _pl() and _read() invocations
466 467 self._pendingmode = None
467 468
468 469 self._use_dirstate_tree = self._ui.configbool(
469 470 b"experimental",
470 471 b"dirstate-tree.in-memory",
471 472 False,
472 473 )
473 474
474 475 def addfile(
475 476 self,
476 477 f,
477 478 state=None,
478 479 mode=0,
479 480 size=None,
480 481 mtime=None,
481 482 added=False,
482 483 merged=False,
483 484 from_p2=False,
484 485 possibly_dirty=False,
485 486 ):
486 487 return self._rustmap.addfile(
487 488 f,
488 489 state,
489 490 mode,
490 491 size,
491 492 mtime,
492 493 added,
493 494 merged,
494 495 from_p2,
495 496 possibly_dirty,
496 497 )
497 498
498 499 def removefile(self, *args, **kwargs):
499 500 return self._rustmap.removefile(*args, **kwargs)
500 501
501 502 def dropfile(self, *args, **kwargs):
502 503 return self._rustmap.dropfile(*args, **kwargs)
503 504
504 505 def clearambiguoustimes(self, *args, **kwargs):
505 506 return self._rustmap.clearambiguoustimes(*args, **kwargs)
506 507
507 508 def nonnormalentries(self):
508 509 return self._rustmap.nonnormalentries()
509 510
510 511 def get(self, *args, **kwargs):
511 512 return self._rustmap.get(*args, **kwargs)
512 513
513 514 @property
514 515 def copymap(self):
515 516 return self._rustmap.copymap()
516 517
517 518 def directories(self):
518 519 return self._rustmap.directories()
519 520
520 521 def preload(self):
521 522 self._rustmap
522 523
523 524 def clear(self):
524 525 self._rustmap.clear()
525 526 self.setparents(
526 527 self._nodeconstants.nullid, self._nodeconstants.nullid
527 528 )
528 529 util.clearcachedproperty(self, b"_dirs")
529 530 util.clearcachedproperty(self, b"_alldirs")
530 531 util.clearcachedproperty(self, b"dirfoldmap")
531 532
532 533 def items(self):
533 534 return self._rustmap.items()
534 535
535 536 def keys(self):
536 537 return iter(self._rustmap)
537 538
538 539 def __contains__(self, key):
539 540 return key in self._rustmap
540 541
541 542 def __getitem__(self, item):
542 543 return self._rustmap[item]
543 544
544 545 def __len__(self):
545 546 return len(self._rustmap)
546 547
547 548 def __iter__(self):
548 549 return iter(self._rustmap)
549 550
550 551 # forward for python2,3 compat
551 552 iteritems = items
552 553
553 554 def _opendirstatefile(self):
554 555 fp, mode = txnutil.trypending(
555 556 self._root, self._opener, self._filename
556 557 )
557 558 if self._pendingmode is not None and self._pendingmode != mode:
558 559 fp.close()
559 560 raise error.Abort(
560 561 _(b'working directory state may be changed parallelly')
561 562 )
562 563 self._pendingmode = mode
563 564 return fp
564 565
565 566 def setparents(self, p1, p2):
566 567 self._parents = (p1, p2)
567 568 self._dirtyparents = True
568 569
569 570 def parents(self):
570 571 if not self._parents:
571 572 if self._use_dirstate_v2:
572 573 offset = len(rustmod.V2_FORMAT_MARKER)
573 574 else:
574 575 offset = 0
575 576 read_len = offset + self._nodelen * 2
576 577 try:
577 578 fp = self._opendirstatefile()
578 579 st = fp.read(read_len)
579 580 fp.close()
580 581 except IOError as err:
581 582 if err.errno != errno.ENOENT:
582 583 raise
583 584 # File doesn't exist, so the current state is empty
584 585 st = b''
585 586
586 587 l = len(st)
587 588 if l == read_len:
588 589 st = st[offset:]
589 590 self._parents = (
590 591 st[: self._nodelen],
591 592 st[self._nodelen : 2 * self._nodelen],
592 593 )
593 594 elif l == 0:
594 595 self._parents = (
595 596 self._nodeconstants.nullid,
596 597 self._nodeconstants.nullid,
597 598 )
598 599 else:
599 600 raise error.Abort(
600 601 _(b'working directory state appears damaged!')
601 602 )
602 603
603 604 return self._parents
604 605
605 606 @propertycache
606 607 def _rustmap(self):
607 608 """
608 609 Fills the Dirstatemap when called.
609 610 """
610 611 # ignore HG_PENDING because identity is used only for writing
611 612 self.identity = util.filestat.frompath(
612 613 self._opener.join(self._filename)
613 614 )
614 615
615 616 try:
616 617 fp = self._opendirstatefile()
617 618 try:
618 619 st = fp.read()
619 620 finally:
620 621 fp.close()
621 622 except IOError as err:
622 623 if err.errno != errno.ENOENT:
623 624 raise
624 625 st = b''
625 626
626 627 self._rustmap, parents = rustmod.DirstateMap.new(
627 628 self._use_dirstate_tree, self._use_dirstate_v2, st
628 629 )
629 630
630 631 if parents and not self._dirtyparents:
631 632 self.setparents(*parents)
632 633
633 634 self.__contains__ = self._rustmap.__contains__
634 635 self.__getitem__ = self._rustmap.__getitem__
635 636 self.get = self._rustmap.get
636 637 return self._rustmap
637 638
638 639 def write(self, st, now):
639 640 parents = self.parents()
640 641 packed = self._rustmap.write(
641 642 self._use_dirstate_v2, parents[0], parents[1], now
642 643 )
643 644 st.write(packed)
644 645 st.close()
645 646 self._dirtyparents = False
646 647
647 648 @propertycache
648 649 def filefoldmap(self):
649 650 """Returns a dictionary mapping normalized case paths to their
650 651 non-normalized versions.
651 652 """
652 653 return self._rustmap.filefoldmapasdict()
653 654
654 655 def hastrackeddir(self, d):
655 656 return self._rustmap.hastrackeddir(d)
656 657
657 658 def hasdir(self, d):
658 659 return self._rustmap.hasdir(d)
659 660
660 661 @propertycache
661 662 def identity(self):
662 663 self._rustmap
663 664 return self.identity
664 665
665 666 @property
666 667 def nonnormalset(self):
667 668 nonnorm = self._rustmap.non_normal_entries()
668 669 return nonnorm
669 670
670 671 @propertycache
671 672 def otherparentset(self):
672 673 otherparents = self._rustmap.other_parent_entries()
673 674 return otherparents
674 675
675 676 def non_normal_or_other_parent_paths(self):
676 677 return self._rustmap.non_normal_or_other_parent_paths()
677 678
678 679 @propertycache
679 680 def dirfoldmap(self):
680 681 f = {}
681 682 normcase = util.normcase
682 683 for name, _pseudo_entry in self.directories():
683 684 f[normcase(name)] = name
684 685 return f
@@ -1,476 +1,477 b''
1 1 // dirstate_map.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
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 use crate::dirstate::parsers::Timestamp;
9 9 use crate::{
10 10 dirstate::EntryState,
11 11 dirstate::MTIME_UNSET,
12 12 dirstate::SIZE_FROM_OTHER_PARENT,
13 13 dirstate::SIZE_NON_NORMAL,
14 14 dirstate::V1_RANGEMASK,
15 15 pack_dirstate, parse_dirstate,
16 16 utils::hg_path::{HgPath, HgPathBuf},
17 17 CopyMap, DirsMultiset, DirstateEntry, DirstateError, DirstateParents,
18 18 StateMap,
19 19 };
20 20 use micro_timer::timed;
21 21 use std::collections::HashSet;
22 22 use std::iter::FromIterator;
23 23 use std::ops::Deref;
24 24
25 25 #[derive(Default)]
26 26 pub struct DirstateMap {
27 27 state_map: StateMap,
28 28 pub copy_map: CopyMap,
29 29 pub dirs: Option<DirsMultiset>,
30 30 pub all_dirs: Option<DirsMultiset>,
31 31 non_normal_set: Option<HashSet<HgPathBuf>>,
32 32 other_parent_set: Option<HashSet<HgPathBuf>>,
33 33 }
34 34
35 35 /// Should only really be used in python interface code, for clarity
36 36 impl Deref for DirstateMap {
37 37 type Target = StateMap;
38 38
39 39 fn deref(&self) -> &Self::Target {
40 40 &self.state_map
41 41 }
42 42 }
43 43
44 44 impl FromIterator<(HgPathBuf, DirstateEntry)> for DirstateMap {
45 45 fn from_iter<I: IntoIterator<Item = (HgPathBuf, DirstateEntry)>>(
46 46 iter: I,
47 47 ) -> Self {
48 48 Self {
49 49 state_map: iter.into_iter().collect(),
50 50 ..Self::default()
51 51 }
52 52 }
53 53 }
54 54
55 55 impl DirstateMap {
56 56 pub fn new() -> Self {
57 57 Self::default()
58 58 }
59 59
60 60 pub fn clear(&mut self) {
61 61 self.state_map = StateMap::default();
62 62 self.copy_map.clear();
63 63 self.non_normal_set = None;
64 64 self.other_parent_set = None;
65 65 }
66 66
67 67 /// Add a tracked file to the dirstate
68 68 pub fn add_file(
69 69 &mut self,
70 70 filename: &HgPath,
71 71 entry: DirstateEntry,
72 72 // XXX once the dust settle this should probably become an enum
73 73 added: bool,
74 74 merged: bool,
75 75 from_p2: bool,
76 76 possibly_dirty: bool,
77 77 ) -> Result<(), DirstateError> {
78 78 let mut entry = entry;
79 79 if added {
80 80 assert!(!merged);
81 81 assert!(!possibly_dirty);
82 82 assert!(!from_p2);
83 83 entry.state = EntryState::Added;
84 84 entry.size = SIZE_NON_NORMAL;
85 85 entry.mtime = MTIME_UNSET;
86 86 } else if merged {
87 87 assert!(!possibly_dirty);
88 88 assert!(!from_p2);
89 89 entry.state = EntryState::Merged;
90 90 entry.size = SIZE_FROM_OTHER_PARENT;
91 91 entry.mtime = MTIME_UNSET;
92 92 } else if from_p2 {
93 93 assert!(!possibly_dirty);
94 entry.state = EntryState::Normal;
94 95 entry.size = SIZE_FROM_OTHER_PARENT;
95 96 entry.mtime = MTIME_UNSET;
96 97 } else if possibly_dirty {
97 98 entry.state = EntryState::Normal;
98 99 entry.size = SIZE_NON_NORMAL;
99 100 entry.mtime = MTIME_UNSET;
100 101 } else {
101 102 entry.size = entry.size & V1_RANGEMASK;
102 103 entry.mtime = entry.mtime & V1_RANGEMASK;
103 104 }
104 105 let old_state = match self.get(filename) {
105 106 Some(e) => e.state,
106 107 None => EntryState::Unknown,
107 108 };
108 109 if old_state == EntryState::Unknown || old_state == EntryState::Removed
109 110 {
110 111 if let Some(ref mut dirs) = self.dirs {
111 112 dirs.add_path(filename)?;
112 113 }
113 114 }
114 115 if old_state == EntryState::Unknown {
115 116 if let Some(ref mut all_dirs) = self.all_dirs {
116 117 all_dirs.add_path(filename)?;
117 118 }
118 119 }
119 120 self.state_map.insert(filename.to_owned(), entry.to_owned());
120 121
121 122 if entry.is_non_normal() {
122 123 self.get_non_normal_other_parent_entries()
123 124 .0
124 125 .insert(filename.to_owned());
125 126 }
126 127
127 128 if entry.is_from_other_parent() {
128 129 self.get_non_normal_other_parent_entries()
129 130 .1
130 131 .insert(filename.to_owned());
131 132 }
132 133 Ok(())
133 134 }
134 135
135 136 /// Mark a file as removed in the dirstate.
136 137 ///
137 138 /// The `size` parameter is used to store sentinel values that indicate
138 139 /// the file's previous state. In the future, we should refactor this
139 140 /// to be more explicit about what that state is.
140 141 pub fn remove_file(
141 142 &mut self,
142 143 filename: &HgPath,
143 144 in_merge: bool,
144 145 ) -> Result<(), DirstateError> {
145 146 let old_entry_opt = self.get(filename);
146 147 let old_state = match old_entry_opt {
147 148 Some(e) => e.state,
148 149 None => EntryState::Unknown,
149 150 };
150 151 let mut size = 0;
151 152 if in_merge {
152 153 // XXX we should not be able to have 'm' state and 'FROM_P2' if not
153 154 // during a merge. So I (marmoute) am not sure we need the
154 155 // conditionnal at all. Adding double checking this with assert
155 156 // would be nice.
156 157 if let Some(old_entry) = old_entry_opt {
157 158 // backup the previous state
158 159 if old_entry.state == EntryState::Merged {
159 160 size = SIZE_NON_NORMAL;
160 161 } else if old_entry.state == EntryState::Normal
161 162 && old_entry.size == SIZE_FROM_OTHER_PARENT
162 163 {
163 164 // other parent
164 165 size = SIZE_FROM_OTHER_PARENT;
165 166 self.get_non_normal_other_parent_entries()
166 167 .1
167 168 .insert(filename.to_owned());
168 169 }
169 170 }
170 171 }
171 172 if old_state != EntryState::Unknown && old_state != EntryState::Removed
172 173 {
173 174 if let Some(ref mut dirs) = self.dirs {
174 175 dirs.delete_path(filename)?;
175 176 }
176 177 }
177 178 if old_state == EntryState::Unknown {
178 179 if let Some(ref mut all_dirs) = self.all_dirs {
179 180 all_dirs.add_path(filename)?;
180 181 }
181 182 }
182 183 if size == 0 {
183 184 self.copy_map.remove(filename);
184 185 }
185 186
186 187 self.state_map.insert(
187 188 filename.to_owned(),
188 189 DirstateEntry {
189 190 state: EntryState::Removed,
190 191 mode: 0,
191 192 size,
192 193 mtime: 0,
193 194 },
194 195 );
195 196 self.get_non_normal_other_parent_entries()
196 197 .0
197 198 .insert(filename.to_owned());
198 199 Ok(())
199 200 }
200 201
201 202 /// Remove a file from the dirstate.
202 203 /// Returns `true` if the file was previously recorded.
203 204 pub fn drop_file(
204 205 &mut self,
205 206 filename: &HgPath,
206 207 old_state: EntryState,
207 208 ) -> Result<bool, DirstateError> {
208 209 let exists = self.state_map.remove(filename).is_some();
209 210
210 211 if exists {
211 212 if old_state != EntryState::Removed {
212 213 if let Some(ref mut dirs) = self.dirs {
213 214 dirs.delete_path(filename)?;
214 215 }
215 216 }
216 217 if let Some(ref mut all_dirs) = self.all_dirs {
217 218 all_dirs.delete_path(filename)?;
218 219 }
219 220 }
220 221 self.get_non_normal_other_parent_entries()
221 222 .0
222 223 .remove(filename);
223 224
224 225 Ok(exists)
225 226 }
226 227
227 228 pub fn clear_ambiguous_times(
228 229 &mut self,
229 230 filenames: Vec<HgPathBuf>,
230 231 now: i32,
231 232 ) {
232 233 for filename in filenames {
233 234 if let Some(entry) = self.state_map.get_mut(&filename) {
234 235 if entry.clear_ambiguous_mtime(now) {
235 236 self.get_non_normal_other_parent_entries()
236 237 .0
237 238 .insert(filename.to_owned());
238 239 }
239 240 }
240 241 }
241 242 }
242 243
243 244 pub fn non_normal_entries_remove(&mut self, key: impl AsRef<HgPath>) {
244 245 self.get_non_normal_other_parent_entries()
245 246 .0
246 247 .remove(key.as_ref());
247 248 }
248 249
249 250 pub fn non_normal_entries_union(
250 251 &mut self,
251 252 other: HashSet<HgPathBuf>,
252 253 ) -> Vec<HgPathBuf> {
253 254 self.get_non_normal_other_parent_entries()
254 255 .0
255 256 .union(&other)
256 257 .map(ToOwned::to_owned)
257 258 .collect()
258 259 }
259 260
260 261 pub fn get_non_normal_other_parent_entries(
261 262 &mut self,
262 263 ) -> (&mut HashSet<HgPathBuf>, &mut HashSet<HgPathBuf>) {
263 264 self.set_non_normal_other_parent_entries(false);
264 265 (
265 266 self.non_normal_set.as_mut().unwrap(),
266 267 self.other_parent_set.as_mut().unwrap(),
267 268 )
268 269 }
269 270
270 271 /// Useful to get immutable references to those sets in contexts where
271 272 /// you only have an immutable reference to the `DirstateMap`, like when
272 273 /// sharing references with Python.
273 274 ///
274 275 /// TODO, get rid of this along with the other "setter/getter" stuff when
275 276 /// a nice typestate plan is defined.
276 277 ///
277 278 /// # Panics
278 279 ///
279 280 /// Will panic if either set is `None`.
280 281 pub fn get_non_normal_other_parent_entries_panic(
281 282 &self,
282 283 ) -> (&HashSet<HgPathBuf>, &HashSet<HgPathBuf>) {
283 284 (
284 285 self.non_normal_set.as_ref().unwrap(),
285 286 self.other_parent_set.as_ref().unwrap(),
286 287 )
287 288 }
288 289
289 290 pub fn set_non_normal_other_parent_entries(&mut self, force: bool) {
290 291 if !force
291 292 && self.non_normal_set.is_some()
292 293 && self.other_parent_set.is_some()
293 294 {
294 295 return;
295 296 }
296 297 let mut non_normal = HashSet::new();
297 298 let mut other_parent = HashSet::new();
298 299
299 300 for (filename, entry) in self.state_map.iter() {
300 301 if entry.is_non_normal() {
301 302 non_normal.insert(filename.to_owned());
302 303 }
303 304 if entry.is_from_other_parent() {
304 305 other_parent.insert(filename.to_owned());
305 306 }
306 307 }
307 308 self.non_normal_set = Some(non_normal);
308 309 self.other_parent_set = Some(other_parent);
309 310 }
310 311
311 312 /// Both of these setters and their uses appear to be the simplest way to
312 313 /// emulate a Python lazy property, but it is ugly and unidiomatic.
313 314 /// TODO One day, rewriting this struct using the typestate might be a
314 315 /// good idea.
315 316 pub fn set_all_dirs(&mut self) -> Result<(), DirstateError> {
316 317 if self.all_dirs.is_none() {
317 318 self.all_dirs = Some(DirsMultiset::from_dirstate(
318 319 self.state_map.iter().map(|(k, v)| Ok((k, *v))),
319 320 None,
320 321 )?);
321 322 }
322 323 Ok(())
323 324 }
324 325
325 326 pub fn set_dirs(&mut self) -> Result<(), DirstateError> {
326 327 if self.dirs.is_none() {
327 328 self.dirs = Some(DirsMultiset::from_dirstate(
328 329 self.state_map.iter().map(|(k, v)| Ok((k, *v))),
329 330 Some(EntryState::Removed),
330 331 )?);
331 332 }
332 333 Ok(())
333 334 }
334 335
335 336 pub fn has_tracked_dir(
336 337 &mut self,
337 338 directory: &HgPath,
338 339 ) -> Result<bool, DirstateError> {
339 340 self.set_dirs()?;
340 341 Ok(self.dirs.as_ref().unwrap().contains(directory))
341 342 }
342 343
343 344 pub fn has_dir(
344 345 &mut self,
345 346 directory: &HgPath,
346 347 ) -> Result<bool, DirstateError> {
347 348 self.set_all_dirs()?;
348 349 Ok(self.all_dirs.as_ref().unwrap().contains(directory))
349 350 }
350 351
351 352 #[timed]
352 353 pub fn read(
353 354 &mut self,
354 355 file_contents: &[u8],
355 356 ) -> Result<Option<DirstateParents>, DirstateError> {
356 357 if file_contents.is_empty() {
357 358 return Ok(None);
358 359 }
359 360
360 361 let (parents, entries, copies) = parse_dirstate(file_contents)?;
361 362 self.state_map.extend(
362 363 entries
363 364 .into_iter()
364 365 .map(|(path, entry)| (path.to_owned(), entry)),
365 366 );
366 367 self.copy_map.extend(
367 368 copies
368 369 .into_iter()
369 370 .map(|(path, copy)| (path.to_owned(), copy.to_owned())),
370 371 );
371 372 Ok(Some(parents.clone()))
372 373 }
373 374
374 375 pub fn pack(
375 376 &mut self,
376 377 parents: DirstateParents,
377 378 now: Timestamp,
378 379 ) -> Result<Vec<u8>, DirstateError> {
379 380 let packed =
380 381 pack_dirstate(&mut self.state_map, &self.copy_map, parents, now)?;
381 382
382 383 self.set_non_normal_other_parent_entries(true);
383 384 Ok(packed)
384 385 }
385 386 }
386 387
387 388 #[cfg(test)]
388 389 mod tests {
389 390 use super::*;
390 391
391 392 #[test]
392 393 fn test_dirs_multiset() {
393 394 let mut map = DirstateMap::new();
394 395 assert!(map.dirs.is_none());
395 396 assert!(map.all_dirs.is_none());
396 397
397 398 assert_eq!(map.has_dir(HgPath::new(b"nope")).unwrap(), false);
398 399 assert!(map.all_dirs.is_some());
399 400 assert!(map.dirs.is_none());
400 401
401 402 assert_eq!(map.has_tracked_dir(HgPath::new(b"nope")).unwrap(), false);
402 403 assert!(map.dirs.is_some());
403 404 }
404 405
405 406 #[test]
406 407 fn test_add_file() {
407 408 let mut map = DirstateMap::new();
408 409
409 410 assert_eq!(0, map.len());
410 411
411 412 map.add_file(
412 413 HgPath::new(b"meh"),
413 414 DirstateEntry {
414 415 state: EntryState::Normal,
415 416 mode: 1337,
416 417 mtime: 1337,
417 418 size: 1337,
418 419 },
419 420 false,
420 421 false,
421 422 false,
422 423 false,
423 424 )
424 425 .unwrap();
425 426
426 427 assert_eq!(1, map.len());
427 428 assert_eq!(0, map.get_non_normal_other_parent_entries().0.len());
428 429 assert_eq!(0, map.get_non_normal_other_parent_entries().1.len());
429 430 }
430 431
431 432 #[test]
432 433 fn test_non_normal_other_parent_entries() {
433 434 let mut map: DirstateMap = [
434 435 (b"f1", (EntryState::Removed, 1337, 1337, 1337)),
435 436 (b"f2", (EntryState::Normal, 1337, 1337, -1)),
436 437 (b"f3", (EntryState::Normal, 1337, 1337, 1337)),
437 438 (b"f4", (EntryState::Normal, 1337, -2, 1337)),
438 439 (b"f5", (EntryState::Added, 1337, 1337, 1337)),
439 440 (b"f6", (EntryState::Added, 1337, 1337, -1)),
440 441 (b"f7", (EntryState::Merged, 1337, 1337, -1)),
441 442 (b"f8", (EntryState::Merged, 1337, 1337, 1337)),
442 443 (b"f9", (EntryState::Merged, 1337, -2, 1337)),
443 444 (b"fa", (EntryState::Added, 1337, -2, 1337)),
444 445 (b"fb", (EntryState::Removed, 1337, -2, 1337)),
445 446 ]
446 447 .iter()
447 448 .map(|(fname, (state, mode, size, mtime))| {
448 449 (
449 450 HgPathBuf::from_bytes(fname.as_ref()),
450 451 DirstateEntry {
451 452 state: *state,
452 453 mode: *mode,
453 454 size: *size,
454 455 mtime: *mtime,
455 456 },
456 457 )
457 458 })
458 459 .collect();
459 460
460 461 let mut non_normal = [
461 462 b"f1", b"f2", b"f5", b"f6", b"f7", b"f8", b"f9", b"fa", b"fb",
462 463 ]
463 464 .iter()
464 465 .map(|x| HgPathBuf::from_bytes(x.as_ref()))
465 466 .collect();
466 467
467 468 let mut other_parent = HashSet::new();
468 469 other_parent.insert(HgPathBuf::from_bytes(b"f4"));
469 470 let entries = map.get_non_normal_other_parent_entries();
470 471
471 472 assert_eq!(
472 473 (&mut non_normal, &mut other_parent),
473 474 (entries.0, entries.1)
474 475 );
475 476 }
476 477 }
@@ -1,1204 +1,1205 b''
1 1 use bytes_cast::BytesCast;
2 2 use micro_timer::timed;
3 3 use std::borrow::Cow;
4 4 use std::convert::TryInto;
5 5 use std::path::PathBuf;
6 6
7 7 use super::on_disk;
8 8 use super::on_disk::DirstateV2ParseError;
9 9 use super::path_with_basename::WithBasename;
10 10 use crate::dirstate::parsers::pack_entry;
11 11 use crate::dirstate::parsers::packed_entry_size;
12 12 use crate::dirstate::parsers::parse_dirstate_entries;
13 13 use crate::dirstate::parsers::Timestamp;
14 14 use crate::dirstate::MTIME_UNSET;
15 15 use crate::dirstate::SIZE_FROM_OTHER_PARENT;
16 16 use crate::dirstate::SIZE_NON_NORMAL;
17 17 use crate::dirstate::V1_RANGEMASK;
18 18 use crate::matchers::Matcher;
19 19 use crate::utils::hg_path::{HgPath, HgPathBuf};
20 20 use crate::CopyMapIter;
21 21 use crate::DirstateEntry;
22 22 use crate::DirstateError;
23 23 use crate::DirstateParents;
24 24 use crate::DirstateStatus;
25 25 use crate::EntryState;
26 26 use crate::FastHashMap;
27 27 use crate::PatternFileWarning;
28 28 use crate::StateMapIter;
29 29 use crate::StatusError;
30 30 use crate::StatusOptions;
31 31
32 32 pub struct DirstateMap<'on_disk> {
33 33 /// Contents of the `.hg/dirstate` file
34 34 pub(super) on_disk: &'on_disk [u8],
35 35
36 36 pub(super) root: ChildNodes<'on_disk>,
37 37
38 38 /// Number of nodes anywhere in the tree that have `.entry.is_some()`.
39 39 pub(super) nodes_with_entry_count: u32,
40 40
41 41 /// Number of nodes anywhere in the tree that have
42 42 /// `.copy_source.is_some()`.
43 43 pub(super) nodes_with_copy_source_count: u32,
44 44
45 45 /// See on_disk::Header
46 46 pub(super) ignore_patterns_hash: on_disk::IgnorePatternsHash,
47 47 }
48 48
49 49 /// Using a plain `HgPathBuf` of the full path from the repository root as a
50 50 /// map key would also work: all paths in a given map have the same parent
51 51 /// path, so comparing full paths gives the same result as comparing base
52 52 /// names. However `HashMap` would waste time always re-hashing the same
53 53 /// string prefix.
54 54 pub(super) type NodeKey<'on_disk> = WithBasename<Cow<'on_disk, HgPath>>;
55 55
56 56 /// Similar to `&'tree Cow<'on_disk, HgPath>`, but can also be returned
57 57 /// for on-disk nodes that don’t actually have a `Cow` to borrow.
58 58 pub(super) enum BorrowedPath<'tree, 'on_disk> {
59 59 InMemory(&'tree HgPathBuf),
60 60 OnDisk(&'on_disk HgPath),
61 61 }
62 62
63 63 pub(super) enum ChildNodes<'on_disk> {
64 64 InMemory(FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
65 65 OnDisk(&'on_disk [on_disk::Node]),
66 66 }
67 67
68 68 pub(super) enum ChildNodesRef<'tree, 'on_disk> {
69 69 InMemory(&'tree FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
70 70 OnDisk(&'on_disk [on_disk::Node]),
71 71 }
72 72
73 73 pub(super) enum NodeRef<'tree, 'on_disk> {
74 74 InMemory(&'tree NodeKey<'on_disk>, &'tree Node<'on_disk>),
75 75 OnDisk(&'on_disk on_disk::Node),
76 76 }
77 77
78 78 impl<'tree, 'on_disk> BorrowedPath<'tree, 'on_disk> {
79 79 pub fn detach_from_tree(&self) -> Cow<'on_disk, HgPath> {
80 80 match *self {
81 81 BorrowedPath::InMemory(in_memory) => Cow::Owned(in_memory.clone()),
82 82 BorrowedPath::OnDisk(on_disk) => Cow::Borrowed(on_disk),
83 83 }
84 84 }
85 85 }
86 86
87 87 impl<'tree, 'on_disk> std::ops::Deref for BorrowedPath<'tree, 'on_disk> {
88 88 type Target = HgPath;
89 89
90 90 fn deref(&self) -> &HgPath {
91 91 match *self {
92 92 BorrowedPath::InMemory(in_memory) => in_memory,
93 93 BorrowedPath::OnDisk(on_disk) => on_disk,
94 94 }
95 95 }
96 96 }
97 97
98 98 impl Default for ChildNodes<'_> {
99 99 fn default() -> Self {
100 100 ChildNodes::InMemory(Default::default())
101 101 }
102 102 }
103 103
104 104 impl<'on_disk> ChildNodes<'on_disk> {
105 105 pub(super) fn as_ref<'tree>(
106 106 &'tree self,
107 107 ) -> ChildNodesRef<'tree, 'on_disk> {
108 108 match self {
109 109 ChildNodes::InMemory(nodes) => ChildNodesRef::InMemory(nodes),
110 110 ChildNodes::OnDisk(nodes) => ChildNodesRef::OnDisk(nodes),
111 111 }
112 112 }
113 113
114 114 pub(super) fn is_empty(&self) -> bool {
115 115 match self {
116 116 ChildNodes::InMemory(nodes) => nodes.is_empty(),
117 117 ChildNodes::OnDisk(nodes) => nodes.is_empty(),
118 118 }
119 119 }
120 120
121 121 pub(super) fn make_mut(
122 122 &mut self,
123 123 on_disk: &'on_disk [u8],
124 124 ) -> Result<
125 125 &mut FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>,
126 126 DirstateV2ParseError,
127 127 > {
128 128 match self {
129 129 ChildNodes::InMemory(nodes) => Ok(nodes),
130 130 ChildNodes::OnDisk(nodes) => {
131 131 let nodes = nodes
132 132 .iter()
133 133 .map(|node| {
134 134 Ok((
135 135 node.path(on_disk)?,
136 136 node.to_in_memory_node(on_disk)?,
137 137 ))
138 138 })
139 139 .collect::<Result<_, _>>()?;
140 140 *self = ChildNodes::InMemory(nodes);
141 141 match self {
142 142 ChildNodes::InMemory(nodes) => Ok(nodes),
143 143 ChildNodes::OnDisk(_) => unreachable!(),
144 144 }
145 145 }
146 146 }
147 147 }
148 148 }
149 149
150 150 impl<'tree, 'on_disk> ChildNodesRef<'tree, 'on_disk> {
151 151 pub(super) fn get(
152 152 &self,
153 153 base_name: &HgPath,
154 154 on_disk: &'on_disk [u8],
155 155 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
156 156 match self {
157 157 ChildNodesRef::InMemory(nodes) => Ok(nodes
158 158 .get_key_value(base_name)
159 159 .map(|(k, v)| NodeRef::InMemory(k, v))),
160 160 ChildNodesRef::OnDisk(nodes) => {
161 161 let mut parse_result = Ok(());
162 162 let search_result = nodes.binary_search_by(|node| {
163 163 match node.base_name(on_disk) {
164 164 Ok(node_base_name) => node_base_name.cmp(base_name),
165 165 Err(e) => {
166 166 parse_result = Err(e);
167 167 // Dummy comparison result, `search_result` won’t
168 168 // be used since `parse_result` is an error
169 169 std::cmp::Ordering::Equal
170 170 }
171 171 }
172 172 });
173 173 parse_result.map(|()| {
174 174 search_result.ok().map(|i| NodeRef::OnDisk(&nodes[i]))
175 175 })
176 176 }
177 177 }
178 178 }
179 179
180 180 /// Iterate in undefined order
181 181 pub(super) fn iter(
182 182 &self,
183 183 ) -> impl Iterator<Item = NodeRef<'tree, 'on_disk>> {
184 184 match self {
185 185 ChildNodesRef::InMemory(nodes) => itertools::Either::Left(
186 186 nodes.iter().map(|(k, v)| NodeRef::InMemory(k, v)),
187 187 ),
188 188 ChildNodesRef::OnDisk(nodes) => {
189 189 itertools::Either::Right(nodes.iter().map(NodeRef::OnDisk))
190 190 }
191 191 }
192 192 }
193 193
194 194 /// Iterate in parallel in undefined order
195 195 pub(super) fn par_iter(
196 196 &self,
197 197 ) -> impl rayon::iter::ParallelIterator<Item = NodeRef<'tree, 'on_disk>>
198 198 {
199 199 use rayon::prelude::*;
200 200 match self {
201 201 ChildNodesRef::InMemory(nodes) => rayon::iter::Either::Left(
202 202 nodes.par_iter().map(|(k, v)| NodeRef::InMemory(k, v)),
203 203 ),
204 204 ChildNodesRef::OnDisk(nodes) => rayon::iter::Either::Right(
205 205 nodes.par_iter().map(NodeRef::OnDisk),
206 206 ),
207 207 }
208 208 }
209 209
210 210 pub(super) fn sorted(&self) -> Vec<NodeRef<'tree, 'on_disk>> {
211 211 match self {
212 212 ChildNodesRef::InMemory(nodes) => {
213 213 let mut vec: Vec<_> = nodes
214 214 .iter()
215 215 .map(|(k, v)| NodeRef::InMemory(k, v))
216 216 .collect();
217 217 fn sort_key<'a>(node: &'a NodeRef) -> &'a HgPath {
218 218 match node {
219 219 NodeRef::InMemory(path, _node) => path.base_name(),
220 220 NodeRef::OnDisk(_) => unreachable!(),
221 221 }
222 222 }
223 223 // `sort_unstable_by_key` doesn’t allow keys borrowing from the
224 224 // value: https://github.com/rust-lang/rust/issues/34162
225 225 vec.sort_unstable_by(|a, b| sort_key(a).cmp(sort_key(b)));
226 226 vec
227 227 }
228 228 ChildNodesRef::OnDisk(nodes) => {
229 229 // Nodes on disk are already sorted
230 230 nodes.iter().map(NodeRef::OnDisk).collect()
231 231 }
232 232 }
233 233 }
234 234 }
235 235
236 236 impl<'tree, 'on_disk> NodeRef<'tree, 'on_disk> {
237 237 pub(super) fn full_path(
238 238 &self,
239 239 on_disk: &'on_disk [u8],
240 240 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
241 241 match self {
242 242 NodeRef::InMemory(path, _node) => Ok(path.full_path()),
243 243 NodeRef::OnDisk(node) => node.full_path(on_disk),
244 244 }
245 245 }
246 246
247 247 /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk,
248 248 /// HgPath>` detached from `'tree`
249 249 pub(super) fn full_path_borrowed(
250 250 &self,
251 251 on_disk: &'on_disk [u8],
252 252 ) -> Result<BorrowedPath<'tree, 'on_disk>, DirstateV2ParseError> {
253 253 match self {
254 254 NodeRef::InMemory(path, _node) => match path.full_path() {
255 255 Cow::Borrowed(on_disk) => Ok(BorrowedPath::OnDisk(on_disk)),
256 256 Cow::Owned(in_memory) => Ok(BorrowedPath::InMemory(in_memory)),
257 257 },
258 258 NodeRef::OnDisk(node) => {
259 259 Ok(BorrowedPath::OnDisk(node.full_path(on_disk)?))
260 260 }
261 261 }
262 262 }
263 263
264 264 pub(super) fn base_name(
265 265 &self,
266 266 on_disk: &'on_disk [u8],
267 267 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
268 268 match self {
269 269 NodeRef::InMemory(path, _node) => Ok(path.base_name()),
270 270 NodeRef::OnDisk(node) => node.base_name(on_disk),
271 271 }
272 272 }
273 273
274 274 pub(super) fn children(
275 275 &self,
276 276 on_disk: &'on_disk [u8],
277 277 ) -> Result<ChildNodesRef<'tree, 'on_disk>, DirstateV2ParseError> {
278 278 match self {
279 279 NodeRef::InMemory(_path, node) => Ok(node.children.as_ref()),
280 280 NodeRef::OnDisk(node) => {
281 281 Ok(ChildNodesRef::OnDisk(node.children(on_disk)?))
282 282 }
283 283 }
284 284 }
285 285
286 286 pub(super) fn has_copy_source(&self) -> bool {
287 287 match self {
288 288 NodeRef::InMemory(_path, node) => node.copy_source.is_some(),
289 289 NodeRef::OnDisk(node) => node.has_copy_source(),
290 290 }
291 291 }
292 292
293 293 pub(super) fn copy_source(
294 294 &self,
295 295 on_disk: &'on_disk [u8],
296 296 ) -> Result<Option<&'tree HgPath>, DirstateV2ParseError> {
297 297 match self {
298 298 NodeRef::InMemory(_path, node) => {
299 299 Ok(node.copy_source.as_ref().map(|s| &**s))
300 300 }
301 301 NodeRef::OnDisk(node) => node.copy_source(on_disk),
302 302 }
303 303 }
304 304
305 305 pub(super) fn entry(
306 306 &self,
307 307 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
308 308 match self {
309 309 NodeRef::InMemory(_path, node) => {
310 310 Ok(node.data.as_entry().copied())
311 311 }
312 312 NodeRef::OnDisk(node) => node.entry(),
313 313 }
314 314 }
315 315
316 316 pub(super) fn state(
317 317 &self,
318 318 ) -> Result<Option<EntryState>, DirstateV2ParseError> {
319 319 match self {
320 320 NodeRef::InMemory(_path, node) => {
321 321 Ok(node.data.as_entry().map(|entry| entry.state))
322 322 }
323 323 NodeRef::OnDisk(node) => node.state(),
324 324 }
325 325 }
326 326
327 327 pub(super) fn cached_directory_mtime(
328 328 &self,
329 329 ) -> Option<&'tree on_disk::Timestamp> {
330 330 match self {
331 331 NodeRef::InMemory(_path, node) => match &node.data {
332 332 NodeData::CachedDirectory { mtime } => Some(mtime),
333 333 _ => None,
334 334 },
335 335 NodeRef::OnDisk(node) => node.cached_directory_mtime(),
336 336 }
337 337 }
338 338
339 339 pub(super) fn descendants_with_entry_count(&self) -> u32 {
340 340 match self {
341 341 NodeRef::InMemory(_path, node) => {
342 342 node.descendants_with_entry_count
343 343 }
344 344 NodeRef::OnDisk(node) => node.descendants_with_entry_count.get(),
345 345 }
346 346 }
347 347
348 348 pub(super) fn tracked_descendants_count(&self) -> u32 {
349 349 match self {
350 350 NodeRef::InMemory(_path, node) => node.tracked_descendants_count,
351 351 NodeRef::OnDisk(node) => node.tracked_descendants_count.get(),
352 352 }
353 353 }
354 354 }
355 355
356 356 /// Represents a file or a directory
357 357 #[derive(Default)]
358 358 pub(super) struct Node<'on_disk> {
359 359 pub(super) data: NodeData,
360 360
361 361 pub(super) copy_source: Option<Cow<'on_disk, HgPath>>,
362 362
363 363 pub(super) children: ChildNodes<'on_disk>,
364 364
365 365 /// How many (non-inclusive) descendants of this node have an entry.
366 366 pub(super) descendants_with_entry_count: u32,
367 367
368 368 /// How many (non-inclusive) descendants of this node have an entry whose
369 369 /// state is "tracked".
370 370 pub(super) tracked_descendants_count: u32,
371 371 }
372 372
373 373 pub(super) enum NodeData {
374 374 Entry(DirstateEntry),
375 375 CachedDirectory { mtime: on_disk::Timestamp },
376 376 None,
377 377 }
378 378
379 379 impl Default for NodeData {
380 380 fn default() -> Self {
381 381 NodeData::None
382 382 }
383 383 }
384 384
385 385 impl NodeData {
386 386 fn has_entry(&self) -> bool {
387 387 match self {
388 388 NodeData::Entry(_) => true,
389 389 _ => false,
390 390 }
391 391 }
392 392
393 393 fn as_entry(&self) -> Option<&DirstateEntry> {
394 394 match self {
395 395 NodeData::Entry(entry) => Some(entry),
396 396 _ => None,
397 397 }
398 398 }
399 399 }
400 400
401 401 impl<'on_disk> DirstateMap<'on_disk> {
402 402 pub(super) fn empty(on_disk: &'on_disk [u8]) -> Self {
403 403 Self {
404 404 on_disk,
405 405 root: ChildNodes::default(),
406 406 nodes_with_entry_count: 0,
407 407 nodes_with_copy_source_count: 0,
408 408 ignore_patterns_hash: [0; on_disk::IGNORE_PATTERNS_HASH_LEN],
409 409 }
410 410 }
411 411
412 412 #[timed]
413 413 pub fn new_v2(
414 414 on_disk: &'on_disk [u8],
415 415 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
416 416 Ok(on_disk::read(on_disk)?)
417 417 }
418 418
419 419 #[timed]
420 420 pub fn new_v1(
421 421 on_disk: &'on_disk [u8],
422 422 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
423 423 let mut map = Self::empty(on_disk);
424 424 if map.on_disk.is_empty() {
425 425 return Ok((map, None));
426 426 }
427 427
428 428 let parents = parse_dirstate_entries(
429 429 map.on_disk,
430 430 |path, entry, copy_source| {
431 431 let tracked = entry.state.is_tracked();
432 432 let node = Self::get_or_insert_node(
433 433 map.on_disk,
434 434 &mut map.root,
435 435 path,
436 436 WithBasename::to_cow_borrowed,
437 437 |ancestor| {
438 438 if tracked {
439 439 ancestor.tracked_descendants_count += 1
440 440 }
441 441 ancestor.descendants_with_entry_count += 1
442 442 },
443 443 )?;
444 444 assert!(
445 445 !node.data.has_entry(),
446 446 "duplicate dirstate entry in read"
447 447 );
448 448 assert!(
449 449 node.copy_source.is_none(),
450 450 "duplicate dirstate entry in read"
451 451 );
452 452 node.data = NodeData::Entry(*entry);
453 453 node.copy_source = copy_source.map(Cow::Borrowed);
454 454 map.nodes_with_entry_count += 1;
455 455 if copy_source.is_some() {
456 456 map.nodes_with_copy_source_count += 1
457 457 }
458 458 Ok(())
459 459 },
460 460 )?;
461 461 let parents = Some(parents.clone());
462 462
463 463 Ok((map, parents))
464 464 }
465 465
466 466 fn get_node<'tree>(
467 467 &'tree self,
468 468 path: &HgPath,
469 469 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
470 470 let mut children = self.root.as_ref();
471 471 let mut components = path.components();
472 472 let mut component =
473 473 components.next().expect("expected at least one components");
474 474 loop {
475 475 if let Some(child) = children.get(component, self.on_disk)? {
476 476 if let Some(next_component) = components.next() {
477 477 component = next_component;
478 478 children = child.children(self.on_disk)?;
479 479 } else {
480 480 return Ok(Some(child));
481 481 }
482 482 } else {
483 483 return Ok(None);
484 484 }
485 485 }
486 486 }
487 487
488 488 /// Returns a mutable reference to the node at `path` if it exists
489 489 ///
490 490 /// This takes `root` instead of `&mut self` so that callers can mutate
491 491 /// other fields while the returned borrow is still valid
492 492 fn get_node_mut<'tree>(
493 493 on_disk: &'on_disk [u8],
494 494 root: &'tree mut ChildNodes<'on_disk>,
495 495 path: &HgPath,
496 496 ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> {
497 497 let mut children = root;
498 498 let mut components = path.components();
499 499 let mut component =
500 500 components.next().expect("expected at least one components");
501 501 loop {
502 502 if let Some(child) = children.make_mut(on_disk)?.get_mut(component)
503 503 {
504 504 if let Some(next_component) = components.next() {
505 505 component = next_component;
506 506 children = &mut child.children;
507 507 } else {
508 508 return Ok(Some(child));
509 509 }
510 510 } else {
511 511 return Ok(None);
512 512 }
513 513 }
514 514 }
515 515
516 516 pub(super) fn get_or_insert<'tree, 'path>(
517 517 &'tree mut self,
518 518 path: &HgPath,
519 519 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
520 520 Self::get_or_insert_node(
521 521 self.on_disk,
522 522 &mut self.root,
523 523 path,
524 524 WithBasename::to_cow_owned,
525 525 |_| {},
526 526 )
527 527 }
528 528
529 529 pub(super) fn get_or_insert_node<'tree, 'path>(
530 530 on_disk: &'on_disk [u8],
531 531 root: &'tree mut ChildNodes<'on_disk>,
532 532 path: &'path HgPath,
533 533 to_cow: impl Fn(
534 534 WithBasename<&'path HgPath>,
535 535 ) -> WithBasename<Cow<'on_disk, HgPath>>,
536 536 mut each_ancestor: impl FnMut(&mut Node),
537 537 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
538 538 let mut child_nodes = root;
539 539 let mut inclusive_ancestor_paths =
540 540 WithBasename::inclusive_ancestors_of(path);
541 541 let mut ancestor_path = inclusive_ancestor_paths
542 542 .next()
543 543 .expect("expected at least one inclusive ancestor");
544 544 loop {
545 545 // TODO: can we avoid allocating an owned key in cases where the
546 546 // map already contains that key, without introducing double
547 547 // lookup?
548 548 let child_node = child_nodes
549 549 .make_mut(on_disk)?
550 550 .entry(to_cow(ancestor_path))
551 551 .or_default();
552 552 if let Some(next) = inclusive_ancestor_paths.next() {
553 553 each_ancestor(child_node);
554 554 ancestor_path = next;
555 555 child_nodes = &mut child_node.children;
556 556 } else {
557 557 return Ok(child_node);
558 558 }
559 559 }
560 560 }
561 561
562 562 fn add_or_remove_file(
563 563 &mut self,
564 564 path: &HgPath,
565 565 old_state: EntryState,
566 566 new_entry: DirstateEntry,
567 567 ) -> Result<(), DirstateV2ParseError> {
568 568 let had_entry = old_state != EntryState::Unknown;
569 569 let tracked_count_increment =
570 570 match (old_state.is_tracked(), new_entry.state.is_tracked()) {
571 571 (false, true) => 1,
572 572 (true, false) => -1,
573 573 _ => 0,
574 574 };
575 575
576 576 let node = Self::get_or_insert_node(
577 577 self.on_disk,
578 578 &mut self.root,
579 579 path,
580 580 WithBasename::to_cow_owned,
581 581 |ancestor| {
582 582 if !had_entry {
583 583 ancestor.descendants_with_entry_count += 1;
584 584 }
585 585
586 586 // We can’t use `+= increment` because the counter is unsigned,
587 587 // and we want debug builds to detect accidental underflow
588 588 // through zero
589 589 match tracked_count_increment {
590 590 1 => ancestor.tracked_descendants_count += 1,
591 591 -1 => ancestor.tracked_descendants_count -= 1,
592 592 _ => {}
593 593 }
594 594 },
595 595 )?;
596 596 if !had_entry {
597 597 self.nodes_with_entry_count += 1
598 598 }
599 599 node.data = NodeData::Entry(new_entry);
600 600 Ok(())
601 601 }
602 602
603 603 fn iter_nodes<'tree>(
604 604 &'tree self,
605 605 ) -> impl Iterator<
606 606 Item = Result<NodeRef<'tree, 'on_disk>, DirstateV2ParseError>,
607 607 > + 'tree {
608 608 // Depth first tree traversal.
609 609 //
610 610 // If we could afford internal iteration and recursion,
611 611 // this would look like:
612 612 //
613 613 // ```
614 614 // fn traverse_children(
615 615 // children: &ChildNodes,
616 616 // each: &mut impl FnMut(&Node),
617 617 // ) {
618 618 // for child in children.values() {
619 619 // traverse_children(&child.children, each);
620 620 // each(child);
621 621 // }
622 622 // }
623 623 // ```
624 624 //
625 625 // However we want an external iterator and therefore can’t use the
626 626 // call stack. Use an explicit stack instead:
627 627 let mut stack = Vec::new();
628 628 let mut iter = self.root.as_ref().iter();
629 629 std::iter::from_fn(move || {
630 630 while let Some(child_node) = iter.next() {
631 631 let children = match child_node.children(self.on_disk) {
632 632 Ok(children) => children,
633 633 Err(error) => return Some(Err(error)),
634 634 };
635 635 // Pseudo-recursion
636 636 let new_iter = children.iter();
637 637 let old_iter = std::mem::replace(&mut iter, new_iter);
638 638 stack.push((child_node, old_iter));
639 639 }
640 640 // Found the end of a `children.iter()` iterator.
641 641 if let Some((child_node, next_iter)) = stack.pop() {
642 642 // "Return" from pseudo-recursion by restoring state from the
643 643 // explicit stack
644 644 iter = next_iter;
645 645
646 646 Some(Ok(child_node))
647 647 } else {
648 648 // Reached the bottom of the stack, we’re done
649 649 None
650 650 }
651 651 })
652 652 }
653 653
654 654 fn clear_known_ambiguous_mtimes(
655 655 &mut self,
656 656 paths: &[impl AsRef<HgPath>],
657 657 ) -> Result<(), DirstateV2ParseError> {
658 658 for path in paths {
659 659 if let Some(node) = Self::get_node_mut(
660 660 self.on_disk,
661 661 &mut self.root,
662 662 path.as_ref(),
663 663 )? {
664 664 if let NodeData::Entry(entry) = &mut node.data {
665 665 entry.clear_mtime();
666 666 }
667 667 }
668 668 }
669 669 Ok(())
670 670 }
671 671
672 672 /// Return a faillilble iterator of full paths of nodes that have an
673 673 /// `entry` for which the given `predicate` returns true.
674 674 ///
675 675 /// Fallibility means that each iterator item is a `Result`, which may
676 676 /// indicate a parse error of the on-disk dirstate-v2 format. Such errors
677 677 /// should only happen if Mercurial is buggy or a repository is corrupted.
678 678 fn filter_full_paths<'tree>(
679 679 &'tree self,
680 680 predicate: impl Fn(&DirstateEntry) -> bool + 'tree,
681 681 ) -> impl Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + 'tree
682 682 {
683 683 filter_map_results(self.iter_nodes(), move |node| {
684 684 if let Some(entry) = node.entry()? {
685 685 if predicate(&entry) {
686 686 return Ok(Some(node.full_path(self.on_disk)?));
687 687 }
688 688 }
689 689 Ok(None)
690 690 })
691 691 }
692 692 }
693 693
694 694 /// Like `Iterator::filter_map`, but over a fallible iterator of `Result`s.
695 695 ///
696 696 /// The callback is only called for incoming `Ok` values. Errors are passed
697 697 /// through as-is. In order to let it use the `?` operator the callback is
698 698 /// expected to return a `Result` of `Option`, instead of an `Option` of
699 699 /// `Result`.
700 700 fn filter_map_results<'a, I, F, A, B, E>(
701 701 iter: I,
702 702 f: F,
703 703 ) -> impl Iterator<Item = Result<B, E>> + 'a
704 704 where
705 705 I: Iterator<Item = Result<A, E>> + 'a,
706 706 F: Fn(A) -> Result<Option<B>, E> + 'a,
707 707 {
708 708 iter.filter_map(move |result| match result {
709 709 Ok(node) => f(node).transpose(),
710 710 Err(e) => Some(Err(e)),
711 711 })
712 712 }
713 713
714 714 impl<'on_disk> super::dispatch::DirstateMapMethods for DirstateMap<'on_disk> {
715 715 fn clear(&mut self) {
716 716 self.root = Default::default();
717 717 self.nodes_with_entry_count = 0;
718 718 self.nodes_with_copy_source_count = 0;
719 719 }
720 720
721 721 fn add_file(
722 722 &mut self,
723 723 filename: &HgPath,
724 724 entry: DirstateEntry,
725 725 added: bool,
726 726 merged: bool,
727 727 from_p2: bool,
728 728 possibly_dirty: bool,
729 729 ) -> Result<(), DirstateError> {
730 730 let mut entry = entry;
731 731 if added {
732 732 assert!(!possibly_dirty);
733 733 assert!(!from_p2);
734 734 entry.state = EntryState::Added;
735 735 entry.size = SIZE_NON_NORMAL;
736 736 entry.mtime = MTIME_UNSET;
737 737 } else if merged {
738 738 assert!(!possibly_dirty);
739 739 assert!(!from_p2);
740 740 entry.state = EntryState::Merged;
741 741 entry.size = SIZE_FROM_OTHER_PARENT;
742 742 entry.mtime = MTIME_UNSET;
743 743 } else if from_p2 {
744 744 assert!(!possibly_dirty);
745 entry.state = EntryState::Normal;
745 746 entry.size = SIZE_FROM_OTHER_PARENT;
746 747 entry.mtime = MTIME_UNSET;
747 748 } else if possibly_dirty {
748 749 entry.state = EntryState::Normal;
749 750 entry.size = SIZE_NON_NORMAL;
750 751 entry.mtime = MTIME_UNSET;
751 752 } else {
752 753 entry.size = entry.size & V1_RANGEMASK;
753 754 entry.mtime = entry.mtime & V1_RANGEMASK;
754 755 }
755 756
756 757 let old_state = match self.get(filename)? {
757 758 Some(e) => e.state,
758 759 None => EntryState::Unknown,
759 760 };
760 761
761 762 Ok(self.add_or_remove_file(filename, old_state, entry)?)
762 763 }
763 764
764 765 fn remove_file(
765 766 &mut self,
766 767 filename: &HgPath,
767 768 in_merge: bool,
768 769 ) -> Result<(), DirstateError> {
769 770 let old_entry_opt = self.get(filename)?;
770 771 let old_state = match old_entry_opt {
771 772 Some(e) => e.state,
772 773 None => EntryState::Unknown,
773 774 };
774 775 let mut size = 0;
775 776 if in_merge {
776 777 // XXX we should not be able to have 'm' state and 'FROM_P2' if not
777 778 // during a merge. So I (marmoute) am not sure we need the
778 779 // conditionnal at all. Adding double checking this with assert
779 780 // would be nice.
780 781 if let Some(old_entry) = old_entry_opt {
781 782 // backup the previous state
782 783 if old_entry.state == EntryState::Merged {
783 784 size = SIZE_NON_NORMAL;
784 785 } else if old_entry.state == EntryState::Normal
785 786 && old_entry.size == SIZE_FROM_OTHER_PARENT
786 787 {
787 788 // other parent
788 789 size = SIZE_FROM_OTHER_PARENT;
789 790 }
790 791 }
791 792 }
792 793 if size == 0 {
793 794 self.copy_map_remove(filename)?;
794 795 }
795 796 let entry = DirstateEntry {
796 797 state: EntryState::Removed,
797 798 mode: 0,
798 799 size,
799 800 mtime: 0,
800 801 };
801 802 Ok(self.add_or_remove_file(filename, old_state, entry)?)
802 803 }
803 804
804 805 fn drop_file(
805 806 &mut self,
806 807 filename: &HgPath,
807 808 old_state: EntryState,
808 809 ) -> Result<bool, DirstateError> {
809 810 struct Dropped {
810 811 was_tracked: bool,
811 812 had_entry: bool,
812 813 had_copy_source: bool,
813 814 }
814 815
815 816 /// If this returns `Ok(Some((dropped, removed)))`, then
816 817 ///
817 818 /// * `dropped` is about the leaf node that was at `filename`
818 819 /// * `removed` is whether this particular level of recursion just
819 820 /// removed a node in `nodes`.
820 821 fn recur<'on_disk>(
821 822 on_disk: &'on_disk [u8],
822 823 nodes: &mut ChildNodes<'on_disk>,
823 824 path: &HgPath,
824 825 ) -> Result<Option<(Dropped, bool)>, DirstateV2ParseError> {
825 826 let (first_path_component, rest_of_path) =
826 827 path.split_first_component();
827 828 let node = if let Some(node) =
828 829 nodes.make_mut(on_disk)?.get_mut(first_path_component)
829 830 {
830 831 node
831 832 } else {
832 833 return Ok(None);
833 834 };
834 835 let dropped;
835 836 if let Some(rest) = rest_of_path {
836 837 if let Some((d, removed)) =
837 838 recur(on_disk, &mut node.children, rest)?
838 839 {
839 840 dropped = d;
840 841 if dropped.had_entry {
841 842 node.descendants_with_entry_count -= 1;
842 843 }
843 844 if dropped.was_tracked {
844 845 node.tracked_descendants_count -= 1;
845 846 }
846 847
847 848 // Directory caches must be invalidated when removing a
848 849 // child node
849 850 if removed {
850 851 if let NodeData::CachedDirectory { .. } = &node.data {
851 852 node.data = NodeData::None
852 853 }
853 854 }
854 855 } else {
855 856 return Ok(None);
856 857 }
857 858 } else {
858 859 let had_entry = node.data.has_entry();
859 860 if had_entry {
860 861 node.data = NodeData::None
861 862 }
862 863 dropped = Dropped {
863 864 was_tracked: node
864 865 .data
865 866 .as_entry()
866 867 .map_or(false, |entry| entry.state.is_tracked()),
867 868 had_entry,
868 869 had_copy_source: node.copy_source.take().is_some(),
869 870 };
870 871 }
871 872 // After recursion, for both leaf (rest_of_path is None) nodes and
872 873 // parent nodes, remove a node if it just became empty.
873 874 let remove = !node.data.has_entry()
874 875 && node.copy_source.is_none()
875 876 && node.children.is_empty();
876 877 if remove {
877 878 nodes.make_mut(on_disk)?.remove(first_path_component);
878 879 }
879 880 Ok(Some((dropped, remove)))
880 881 }
881 882
882 883 if let Some((dropped, _removed)) =
883 884 recur(self.on_disk, &mut self.root, filename)?
884 885 {
885 886 if dropped.had_entry {
886 887 self.nodes_with_entry_count -= 1
887 888 }
888 889 if dropped.had_copy_source {
889 890 self.nodes_with_copy_source_count -= 1
890 891 }
891 892 Ok(dropped.had_entry)
892 893 } else {
893 894 debug_assert!(!old_state.is_tracked());
894 895 Ok(false)
895 896 }
896 897 }
897 898
898 899 fn clear_ambiguous_times(
899 900 &mut self,
900 901 filenames: Vec<HgPathBuf>,
901 902 now: i32,
902 903 ) -> Result<(), DirstateV2ParseError> {
903 904 for filename in filenames {
904 905 if let Some(node) =
905 906 Self::get_node_mut(self.on_disk, &mut self.root, &filename)?
906 907 {
907 908 if let NodeData::Entry(entry) = &mut node.data {
908 909 entry.clear_ambiguous_mtime(now);
909 910 }
910 911 }
911 912 }
912 913 Ok(())
913 914 }
914 915
915 916 fn non_normal_entries_contains(
916 917 &mut self,
917 918 key: &HgPath,
918 919 ) -> Result<bool, DirstateV2ParseError> {
919 920 Ok(if let Some(node) = self.get_node(key)? {
920 921 node.entry()?.map_or(false, |entry| entry.is_non_normal())
921 922 } else {
922 923 false
923 924 })
924 925 }
925 926
926 927 fn non_normal_entries_remove(&mut self, _key: &HgPath) {
927 928 // Do nothing, this `DirstateMap` does not have a separate "non normal
928 929 // entries" set that need to be kept up to date
929 930 }
930 931
931 932 fn non_normal_or_other_parent_paths(
932 933 &mut self,
933 934 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
934 935 {
935 936 Box::new(self.filter_full_paths(|entry| {
936 937 entry.is_non_normal() || entry.is_from_other_parent()
937 938 }))
938 939 }
939 940
940 941 fn set_non_normal_other_parent_entries(&mut self, _force: bool) {
941 942 // Do nothing, this `DirstateMap` does not have a separate "non normal
942 943 // entries" and "from other parent" sets that need to be recomputed
943 944 }
944 945
945 946 fn iter_non_normal_paths(
946 947 &mut self,
947 948 ) -> Box<
948 949 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
949 950 > {
950 951 self.iter_non_normal_paths_panic()
951 952 }
952 953
953 954 fn iter_non_normal_paths_panic(
954 955 &self,
955 956 ) -> Box<
956 957 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
957 958 > {
958 959 Box::new(self.filter_full_paths(|entry| entry.is_non_normal()))
959 960 }
960 961
961 962 fn iter_other_parent_paths(
962 963 &mut self,
963 964 ) -> Box<
964 965 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
965 966 > {
966 967 Box::new(self.filter_full_paths(|entry| entry.is_from_other_parent()))
967 968 }
968 969
969 970 fn has_tracked_dir(
970 971 &mut self,
971 972 directory: &HgPath,
972 973 ) -> Result<bool, DirstateError> {
973 974 if let Some(node) = self.get_node(directory)? {
974 975 // A node without a `DirstateEntry` was created to hold child
975 976 // nodes, and is therefore a directory.
976 977 let state = node.state()?;
977 978 Ok(state.is_none() && node.tracked_descendants_count() > 0)
978 979 } else {
979 980 Ok(false)
980 981 }
981 982 }
982 983
983 984 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
984 985 if let Some(node) = self.get_node(directory)? {
985 986 // A node without a `DirstateEntry` was created to hold child
986 987 // nodes, and is therefore a directory.
987 988 let state = node.state()?;
988 989 Ok(state.is_none() && node.descendants_with_entry_count() > 0)
989 990 } else {
990 991 Ok(false)
991 992 }
992 993 }
993 994
994 995 #[timed]
995 996 fn pack_v1(
996 997 &mut self,
997 998 parents: DirstateParents,
998 999 now: Timestamp,
999 1000 ) -> Result<Vec<u8>, DirstateError> {
1000 1001 let now: i32 = now.0.try_into().expect("time overflow");
1001 1002 let mut ambiguous_mtimes = Vec::new();
1002 1003 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
1003 1004 // reallocations
1004 1005 let mut size = parents.as_bytes().len();
1005 1006 for node in self.iter_nodes() {
1006 1007 let node = node?;
1007 1008 if let Some(entry) = node.entry()? {
1008 1009 size += packed_entry_size(
1009 1010 node.full_path(self.on_disk)?,
1010 1011 node.copy_source(self.on_disk)?,
1011 1012 );
1012 1013 if entry.mtime_is_ambiguous(now) {
1013 1014 ambiguous_mtimes.push(
1014 1015 node.full_path_borrowed(self.on_disk)?
1015 1016 .detach_from_tree(),
1016 1017 )
1017 1018 }
1018 1019 }
1019 1020 }
1020 1021 self.clear_known_ambiguous_mtimes(&ambiguous_mtimes)?;
1021 1022
1022 1023 let mut packed = Vec::with_capacity(size);
1023 1024 packed.extend(parents.as_bytes());
1024 1025
1025 1026 for node in self.iter_nodes() {
1026 1027 let node = node?;
1027 1028 if let Some(entry) = node.entry()? {
1028 1029 pack_entry(
1029 1030 node.full_path(self.on_disk)?,
1030 1031 &entry,
1031 1032 node.copy_source(self.on_disk)?,
1032 1033 &mut packed,
1033 1034 );
1034 1035 }
1035 1036 }
1036 1037 Ok(packed)
1037 1038 }
1038 1039
1039 1040 #[timed]
1040 1041 fn pack_v2(
1041 1042 &mut self,
1042 1043 parents: DirstateParents,
1043 1044 now: Timestamp,
1044 1045 ) -> Result<Vec<u8>, DirstateError> {
1045 1046 // TODO:Β how do we want to handle this in 2038?
1046 1047 let now: i32 = now.0.try_into().expect("time overflow");
1047 1048 let mut paths = Vec::new();
1048 1049 for node in self.iter_nodes() {
1049 1050 let node = node?;
1050 1051 if let Some(entry) = node.entry()? {
1051 1052 if entry.mtime_is_ambiguous(now) {
1052 1053 paths.push(
1053 1054 node.full_path_borrowed(self.on_disk)?
1054 1055 .detach_from_tree(),
1055 1056 )
1056 1057 }
1057 1058 }
1058 1059 }
1059 1060 // Borrow of `self` ends here since we collect cloned paths
1060 1061
1061 1062 self.clear_known_ambiguous_mtimes(&paths)?;
1062 1063
1063 1064 on_disk::write(self, parents)
1064 1065 }
1065 1066
1066 1067 fn status<'a>(
1067 1068 &'a mut self,
1068 1069 matcher: &'a (dyn Matcher + Sync),
1069 1070 root_dir: PathBuf,
1070 1071 ignore_files: Vec<PathBuf>,
1071 1072 options: StatusOptions,
1072 1073 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
1073 1074 {
1074 1075 super::status::status(self, matcher, root_dir, ignore_files, options)
1075 1076 }
1076 1077
1077 1078 fn copy_map_len(&self) -> usize {
1078 1079 self.nodes_with_copy_source_count as usize
1079 1080 }
1080 1081
1081 1082 fn copy_map_iter(&self) -> CopyMapIter<'_> {
1082 1083 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1083 1084 Ok(if let Some(source) = node.copy_source(self.on_disk)? {
1084 1085 Some((node.full_path(self.on_disk)?, source))
1085 1086 } else {
1086 1087 None
1087 1088 })
1088 1089 }))
1089 1090 }
1090 1091
1091 1092 fn copy_map_contains_key(
1092 1093 &self,
1093 1094 key: &HgPath,
1094 1095 ) -> Result<bool, DirstateV2ParseError> {
1095 1096 Ok(if let Some(node) = self.get_node(key)? {
1096 1097 node.has_copy_source()
1097 1098 } else {
1098 1099 false
1099 1100 })
1100 1101 }
1101 1102
1102 1103 fn copy_map_get(
1103 1104 &self,
1104 1105 key: &HgPath,
1105 1106 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
1106 1107 if let Some(node) = self.get_node(key)? {
1107 1108 if let Some(source) = node.copy_source(self.on_disk)? {
1108 1109 return Ok(Some(source));
1109 1110 }
1110 1111 }
1111 1112 Ok(None)
1112 1113 }
1113 1114
1114 1115 fn copy_map_remove(
1115 1116 &mut self,
1116 1117 key: &HgPath,
1117 1118 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1118 1119 let count = &mut self.nodes_with_copy_source_count;
1119 1120 Ok(
1120 1121 Self::get_node_mut(self.on_disk, &mut self.root, key)?.and_then(
1121 1122 |node| {
1122 1123 if node.copy_source.is_some() {
1123 1124 *count -= 1
1124 1125 }
1125 1126 node.copy_source.take().map(Cow::into_owned)
1126 1127 },
1127 1128 ),
1128 1129 )
1129 1130 }
1130 1131
1131 1132 fn copy_map_insert(
1132 1133 &mut self,
1133 1134 key: HgPathBuf,
1134 1135 value: HgPathBuf,
1135 1136 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1136 1137 let node = Self::get_or_insert_node(
1137 1138 self.on_disk,
1138 1139 &mut self.root,
1139 1140 &key,
1140 1141 WithBasename::to_cow_owned,
1141 1142 |_ancestor| {},
1142 1143 )?;
1143 1144 if node.copy_source.is_none() {
1144 1145 self.nodes_with_copy_source_count += 1
1145 1146 }
1146 1147 Ok(node.copy_source.replace(value.into()).map(Cow::into_owned))
1147 1148 }
1148 1149
1149 1150 fn len(&self) -> usize {
1150 1151 self.nodes_with_entry_count as usize
1151 1152 }
1152 1153
1153 1154 fn contains_key(
1154 1155 &self,
1155 1156 key: &HgPath,
1156 1157 ) -> Result<bool, DirstateV2ParseError> {
1157 1158 Ok(self.get(key)?.is_some())
1158 1159 }
1159 1160
1160 1161 fn get(
1161 1162 &self,
1162 1163 key: &HgPath,
1163 1164 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
1164 1165 Ok(if let Some(node) = self.get_node(key)? {
1165 1166 node.entry()?
1166 1167 } else {
1167 1168 None
1168 1169 })
1169 1170 }
1170 1171
1171 1172 fn iter(&self) -> StateMapIter<'_> {
1172 1173 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1173 1174 Ok(if let Some(entry) = node.entry()? {
1174 1175 Some((node.full_path(self.on_disk)?, entry))
1175 1176 } else {
1176 1177 None
1177 1178 })
1178 1179 }))
1179 1180 }
1180 1181
1181 1182 fn iter_directories(
1182 1183 &self,
1183 1184 ) -> Box<
1184 1185 dyn Iterator<
1185 1186 Item = Result<
1186 1187 (&HgPath, Option<Timestamp>),
1187 1188 DirstateV2ParseError,
1188 1189 >,
1189 1190 > + Send
1190 1191 + '_,
1191 1192 > {
1192 1193 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1193 1194 Ok(if node.state()?.is_none() {
1194 1195 Some((
1195 1196 node.full_path(self.on_disk)?,
1196 1197 node.cached_directory_mtime()
1197 1198 .map(|mtime| Timestamp(mtime.seconds())),
1198 1199 ))
1199 1200 } else {
1200 1201 None
1201 1202 })
1202 1203 }))
1203 1204 }
1204 1205 }
General Comments 0
You need to be logged in to leave comments. Login now