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