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