comparison mercurial/dirstatemap.py @ 48123:771c90807a2b

dirstatemap: arrange methods by category The dirstatemap code cover various aspects, it grow a bit messy over the years. So we shuffle the code around into some documented categories. This will help use to clean up the code. No code was changed in this changeset, only code move. Differential Revision: https://phab.mercurial-scm.org/D11568
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Sat, 02 Oct 2021 12:10:46 +0200
parents bbd924a36a6e
children 08e04bb0bff3
comparison
equal deleted inserted replaced
48122:bbd924a36a6e 48123:771c90807a2b
121 if self._use_dirstate_v2: 121 if self._use_dirstate_v2:
122 msg = "Dirstate V2 not supportedi" 122 msg = "Dirstate V2 not supportedi"
123 msg += "(should have detected unsupported requirement)" 123 msg += "(should have detected unsupported requirement)"
124 raise error.ProgrammingError(msg) 124 raise error.ProgrammingError(msg)
125 125
126 ### Core data storage and access
127
126 @propertycache 128 @propertycache
127 def _map(self): 129 def _map(self):
128 self._map = {} 130 self._map = {}
129 self.read() 131 self.read()
130 return self._map 132 return self._map
159 for (filename, item) in self.items(): 161 for (filename, item) in self.items():
160 yield (filename, item.state, item.mode, item.size, item.mtime) 162 yield (filename, item.state, item.mode, item.size, item.mtime)
161 163
162 def keys(self): 164 def keys(self):
163 return self._map.keys() 165 return self._map.keys()
166
167 ### reading/setting parents
168
169 def parents(self):
170 if not self._parents:
171 try:
172 fp = self._opendirstatefile()
173 st = fp.read(2 * self._nodelen)
174 fp.close()
175 except IOError as err:
176 if err.errno != errno.ENOENT:
177 raise
178 # File doesn't exist, so the current state is empty
179 st = b''
180
181 l = len(st)
182 if l == self._nodelen * 2:
183 self._parents = (
184 st[: self._nodelen],
185 st[self._nodelen : 2 * self._nodelen],
186 )
187 elif l == 0:
188 self._parents = (
189 self._nodeconstants.nullid,
190 self._nodeconstants.nullid,
191 )
192 else:
193 raise error.Abort(
194 _(b'working directory state appears damaged!')
195 )
196
197 return self._parents
198
199 def setparents(self, p1, p2, fold_p2=False):
200 self._parents = (p1, p2)
201 self._dirtyparents = True
202 copies = {}
203 if fold_p2:
204 for f, s in pycompat.iteritems(self._map):
205 # Discard "merged" markers when moving away from a merge state
206 if s.merged or s.from_p2:
207 source = self.copymap.pop(f, None)
208 if source:
209 copies[f] = source
210 s.drop_merge_data()
211 return copies
212
213 ### disk interaction
214
215 def read(self):
216 # ignore HG_PENDING because identity is used only for writing
217 self.identity = util.filestat.frompath(
218 self._opener.join(self._filename)
219 )
220
221 try:
222 fp = self._opendirstatefile()
223 try:
224 st = fp.read()
225 finally:
226 fp.close()
227 except IOError as err:
228 if err.errno != errno.ENOENT:
229 raise
230 return
231 if not st:
232 return
233
234 if util.safehasattr(parsers, b'dict_new_presized'):
235 # Make an estimate of the number of files in the dirstate based on
236 # its size. This trades wasting some memory for avoiding costly
237 # resizes. Each entry have a prefix of 17 bytes followed by one or
238 # two path names. Studies on various large-scale real-world repositories
239 # found 54 bytes a reasonable upper limit for the average path names.
240 # Copy entries are ignored for the sake of this estimate.
241 self._map = parsers.dict_new_presized(len(st) // 71)
242
243 # Python's garbage collector triggers a GC each time a certain number
244 # of container objects (the number being defined by
245 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
246 # for each file in the dirstate. The C version then immediately marks
247 # them as not to be tracked by the collector. However, this has no
248 # effect on when GCs are triggered, only on what objects the GC looks
249 # into. This means that O(number of files) GCs are unavoidable.
250 # Depending on when in the process's lifetime the dirstate is parsed,
251 # this can get very expensive. As a workaround, disable GC while
252 # parsing the dirstate.
253 #
254 # (we cannot decorate the function directly since it is in a C module)
255 parse_dirstate = util.nogc(parsers.parse_dirstate)
256 p = parse_dirstate(self._map, self.copymap, st)
257 if not self._dirtyparents:
258 self.setparents(*p)
259
260 # Avoid excess attribute lookups by fast pathing certain checks
261 self.__contains__ = self._map.__contains__
262 self.__getitem__ = self._map.__getitem__
263 self.get = self._map.get
264
265 def write(self, _tr, st, now):
266 d = parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
267 st.write(d)
268 st.close()
269 self._dirtyparents = False
270
271 def _opendirstatefile(self):
272 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
273 if self._pendingmode is not None and self._pendingmode != mode:
274 fp.close()
275 raise error.Abort(
276 _(b'working directory state may be changed parallelly')
277 )
278 self._pendingmode = mode
279 return fp
280
281 @propertycache
282 def identity(self):
283 self._map
284 return self.identity
285
286 ### code related to maintaining and accessing "extra" property
287 # (e.g. "has_dir")
164 288
165 def _dirs_incr(self, filename, old_entry=None): 289 def _dirs_incr(self, filename, old_entry=None):
166 """incremente the dirstate counter if applicable""" 290 """incremente the dirstate counter if applicable"""
167 if ( 291 if (
168 old_entry is None or old_entry.removed 292 old_entry is None or old_entry.removed
181 elif remove_variant and "_alldirs" in self.__dict__: 305 elif remove_variant and "_alldirs" in self.__dict__:
182 self._alldirs.addpath(filename) 306 self._alldirs.addpath(filename)
183 if "filefoldmap" in self.__dict__: 307 if "filefoldmap" in self.__dict__:
184 normed = util.normcase(filename) 308 normed = util.normcase(filename)
185 self.filefoldmap.pop(normed, None) 309 self.filefoldmap.pop(normed, None)
310
311 @propertycache
312 def filefoldmap(self):
313 """Returns a dictionary mapping normalized case paths to their
314 non-normalized versions.
315 """
316 try:
317 makefilefoldmap = parsers.make_file_foldmap
318 except AttributeError:
319 pass
320 else:
321 return makefilefoldmap(
322 self._map, util.normcasespec, util.normcasefallback
323 )
324
325 f = {}
326 normcase = util.normcase
327 for name, s in pycompat.iteritems(self._map):
328 if not s.removed:
329 f[normcase(name)] = name
330 f[b'.'] = b'.' # prevents useless util.fspath() invocation
331 return f
332
333 @propertycache
334 def dirfoldmap(self):
335 f = {}
336 normcase = util.normcase
337 for name in self._dirs:
338 f[normcase(name)] = name
339 return f
340
341 def hastrackeddir(self, d):
342 """
343 Returns True if the dirstate contains a tracked (not removed) file
344 in this directory.
345 """
346 return d in self._dirs
347
348 def hasdir(self, d):
349 """
350 Returns True if the dirstate contains a file (tracked or removed)
351 in this directory.
352 """
353 return d in self._alldirs
354
355 @propertycache
356 def _dirs(self):
357 return pathutil.dirs(self._map, only_tracked=True)
358
359 @propertycache
360 def _alldirs(self):
361 return pathutil.dirs(self._map)
362
363 ### code related to manipulation of entries and copy-sources
186 364
187 def set_possibly_dirty(self, filename): 365 def set_possibly_dirty(self, filename):
188 """record that the current state of the file on disk is unknown""" 366 """record that the current state of the file on disk is unknown"""
189 self[filename].set_possibly_dirty() 367 self[filename].set_possibly_dirty()
190 368
305 self._map.pop(f, None) 483 self._map.pop(f, None)
306 else: 484 else:
307 entry.set_untracked() 485 entry.set_untracked()
308 return True 486 return True
309 487
310 @propertycache
311 def filefoldmap(self):
312 """Returns a dictionary mapping normalized case paths to their
313 non-normalized versions.
314 """
315 try:
316 makefilefoldmap = parsers.make_file_foldmap
317 except AttributeError:
318 pass
319 else:
320 return makefilefoldmap(
321 self._map, util.normcasespec, util.normcasefallback
322 )
323
324 f = {}
325 normcase = util.normcase
326 for name, s in pycompat.iteritems(self._map):
327 if not s.removed:
328 f[normcase(name)] = name
329 f[b'.'] = b'.' # prevents useless util.fspath() invocation
330 return f
331
332 def hastrackeddir(self, d):
333 """
334 Returns True if the dirstate contains a tracked (not removed) file
335 in this directory.
336 """
337 return d in self._dirs
338
339 def hasdir(self, d):
340 """
341 Returns True if the dirstate contains a file (tracked or removed)
342 in this directory.
343 """
344 return d in self._alldirs
345
346 @propertycache
347 def _dirs(self):
348 return pathutil.dirs(self._map, only_tracked=True)
349
350 @propertycache
351 def _alldirs(self):
352 return pathutil.dirs(self._map)
353
354 def _opendirstatefile(self):
355 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
356 if self._pendingmode is not None and self._pendingmode != mode:
357 fp.close()
358 raise error.Abort(
359 _(b'working directory state may be changed parallelly')
360 )
361 self._pendingmode = mode
362 return fp
363
364 def parents(self):
365 if not self._parents:
366 try:
367 fp = self._opendirstatefile()
368 st = fp.read(2 * self._nodelen)
369 fp.close()
370 except IOError as err:
371 if err.errno != errno.ENOENT:
372 raise
373 # File doesn't exist, so the current state is empty
374 st = b''
375
376 l = len(st)
377 if l == self._nodelen * 2:
378 self._parents = (
379 st[: self._nodelen],
380 st[self._nodelen : 2 * self._nodelen],
381 )
382 elif l == 0:
383 self._parents = (
384 self._nodeconstants.nullid,
385 self._nodeconstants.nullid,
386 )
387 else:
388 raise error.Abort(
389 _(b'working directory state appears damaged!')
390 )
391
392 return self._parents
393
394 def setparents(self, p1, p2, fold_p2=False):
395 self._parents = (p1, p2)
396 self._dirtyparents = True
397 copies = {}
398 if fold_p2:
399 for f, s in pycompat.iteritems(self._map):
400 # Discard "merged" markers when moving away from a merge state
401 if s.merged or s.from_p2:
402 source = self.copymap.pop(f, None)
403 if source:
404 copies[f] = source
405 s.drop_merge_data()
406 return copies
407
408 def read(self):
409 # ignore HG_PENDING because identity is used only for writing
410 self.identity = util.filestat.frompath(
411 self._opener.join(self._filename)
412 )
413
414 try:
415 fp = self._opendirstatefile()
416 try:
417 st = fp.read()
418 finally:
419 fp.close()
420 except IOError as err:
421 if err.errno != errno.ENOENT:
422 raise
423 return
424 if not st:
425 return
426
427 if util.safehasattr(parsers, b'dict_new_presized'):
428 # Make an estimate of the number of files in the dirstate based on
429 # its size. This trades wasting some memory for avoiding costly
430 # resizes. Each entry have a prefix of 17 bytes followed by one or
431 # two path names. Studies on various large-scale real-world repositories
432 # found 54 bytes a reasonable upper limit for the average path names.
433 # Copy entries are ignored for the sake of this estimate.
434 self._map = parsers.dict_new_presized(len(st) // 71)
435
436 # Python's garbage collector triggers a GC each time a certain number
437 # of container objects (the number being defined by
438 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
439 # for each file in the dirstate. The C version then immediately marks
440 # them as not to be tracked by the collector. However, this has no
441 # effect on when GCs are triggered, only on what objects the GC looks
442 # into. This means that O(number of files) GCs are unavoidable.
443 # Depending on when in the process's lifetime the dirstate is parsed,
444 # this can get very expensive. As a workaround, disable GC while
445 # parsing the dirstate.
446 #
447 # (we cannot decorate the function directly since it is in a C module)
448 parse_dirstate = util.nogc(parsers.parse_dirstate)
449 p = parse_dirstate(self._map, self.copymap, st)
450 if not self._dirtyparents:
451 self.setparents(*p)
452
453 # Avoid excess attribute lookups by fast pathing certain checks
454 self.__contains__ = self._map.__contains__
455 self.__getitem__ = self._map.__getitem__
456 self.get = self._map.get
457
458 def write(self, _tr, st, now):
459 st.write(
460 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
461 )
462 st.close()
463 self._dirtyparents = False
464
465 @propertycache
466 def identity(self):
467 self._map
468 return self.identity
469
470 @propertycache
471 def dirfoldmap(self):
472 f = {}
473 normcase = util.normcase
474 for name in self._dirs:
475 f[normcase(name)] = name
476 return f
477
478 488
479 if rustmod is not None: 489 if rustmod is not None:
480 490
481 class dirstatemap(_dirstatemapcommon): 491 class dirstatemap(_dirstatemapcommon):
482 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2): 492 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
483 super(dirstatemap, self).__init__( 493 super(dirstatemap, self).__init__(
484 ui, opener, root, nodeconstants, use_dirstate_v2 494 ui, opener, root, nodeconstants, use_dirstate_v2
485 ) 495 )
486 self._docket = None 496 self._docket = None
487 497
488 def addfile( 498 ### Core data storage and access
489 self, 499
490 f, 500 @property
491 mode=0, 501 def docket(self):
492 size=None, 502 if not self._docket:
493 mtime=None, 503 if not self._use_dirstate_v2:
494 added=False, 504 raise error.ProgrammingError(
495 merged=False, 505 b'dirstate only has a docket in v2 format'
496 from_p2=False, 506 )
497 possibly_dirty=False, 507 self._docket = docketmod.DirstateDocket.parse(
498 ): 508 self._readdirstatefile(), self._nodeconstants
499 if added: 509 )
500 assert not possibly_dirty 510 return self._docket
501 assert not from_p2 511
502 item = DirstateItem.new_added() 512 @propertycache
503 elif merged: 513 def _map(self):
504 assert not possibly_dirty 514 """
505 assert not from_p2 515 Fills the Dirstatemap when called.
506 item = DirstateItem.new_merged() 516 """
507 elif from_p2: 517 # ignore HG_PENDING because identity is used only for writing
508 assert not possibly_dirty 518 self.identity = util.filestat.frompath(
509 item = DirstateItem.new_from_p2() 519 self._opener.join(self._filename)
510 elif possibly_dirty: 520 )
511 item = DirstateItem.new_possibly_dirty() 521
522 if self._use_dirstate_v2:
523 if self.docket.uuid:
524 # TODO: use mmap when possible
525 data = self._opener.read(self.docket.data_filename())
526 else:
527 data = b''
528 self._map = rustmod.DirstateMap.new_v2(
529 data, self.docket.data_size, self.docket.tree_metadata
530 )
531 parents = self.docket.parents
512 else: 532 else:
513 assert size is not None 533 self._map, parents = rustmod.DirstateMap.new_v1(
514 assert mtime is not None 534 self._readdirstatefile()
515 size = size & rangemask 535 )
516 mtime = mtime & rangemask 536
517 item = DirstateItem.new_normal(mode, size, mtime) 537 if parents and not self._dirtyparents:
518 self._map.addfile(f, item) 538 self.setparents(*parents)
519 if added: 539
520 self.copymap.pop(f, None) 540 self.__contains__ = self._map.__contains__
521 541 self.__getitem__ = self._map.__getitem__
522 def reset_state( 542 self.get = self._map.get
523 self, 543 return self._map
524 filename,
525 wc_tracked=False,
526 p1_tracked=False,
527 p2_tracked=False,
528 merged=False,
529 clean_p1=False,
530 clean_p2=False,
531 possibly_dirty=False,
532 parentfiledata=None,
533 ):
534 """Set a entry to a given state, disregarding all previous state
535
536 This is to be used by the part of the dirstate API dedicated to
537 adjusting the dirstate after a update/merge.
538
539 note: calling this might result to no entry existing at all if the
540 dirstate map does not see any point at having one for this file
541 anymore.
542 """
543 if merged and (clean_p1 or clean_p2):
544 msg = (
545 b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
546 )
547 raise error.ProgrammingError(msg)
548 # copy information are now outdated
549 # (maybe new information should be in directly passed to this function)
550 self.copymap.pop(filename, None)
551
552 if not (p1_tracked or p2_tracked or wc_tracked):
553 self._map.drop_item_and_copy_source(filename)
554 elif merged:
555 # XXX might be merged and removed ?
556 entry = self.get(filename)
557 if entry is not None and entry.tracked:
558 # XXX mostly replicate dirstate.other parent. We should get
559 # the higher layer to pass us more reliable data where `merged`
560 # actually mean merged. Dropping the else clause will show
561 # failure in `test-graft.t`
562 self.addfile(filename, merged=True)
563 else:
564 self.addfile(filename, from_p2=True)
565 elif not (p1_tracked or p2_tracked) and wc_tracked:
566 self.addfile(
567 filename, added=True, possibly_dirty=possibly_dirty
568 )
569 elif (p1_tracked or p2_tracked) and not wc_tracked:
570 # XXX might be merged and removed ?
571 self[filename] = DirstateItem.from_v1_data(b'r', 0, 0, 0)
572 elif clean_p2 and wc_tracked:
573 if p1_tracked or self.get(filename) is not None:
574 # XXX the `self.get` call is catching some case in
575 # `test-merge-remove.t` where the file is tracked in p1, the
576 # p1_tracked argument is False.
577 #
578 # In addition, this seems to be a case where the file is marked
579 # as merged without actually being the result of a merge
580 # action. So thing are not ideal here.
581 self.addfile(filename, merged=True)
582 else:
583 self.addfile(filename, from_p2=True)
584 elif not p1_tracked and p2_tracked and wc_tracked:
585 self.addfile(
586 filename, from_p2=True, possibly_dirty=possibly_dirty
587 )
588 elif possibly_dirty:
589 self.addfile(filename, possibly_dirty=possibly_dirty)
590 elif wc_tracked:
591 # this is a "normal" file
592 if parentfiledata is None:
593 msg = b'failed to pass parentfiledata for a normal file: %s'
594 msg %= filename
595 raise error.ProgrammingError(msg)
596 mode, size, mtime = parentfiledata
597 self.addfile(filename, mode=mode, size=size, mtime=mtime)
598 else:
599 assert False, 'unreachable'
600
601 def set_tracked(self, filename):
602 new = False
603 entry = self.get(filename)
604 if entry is None:
605 self.addfile(filename, added=True)
606 new = True
607 elif not entry.tracked:
608 entry.set_tracked()
609 self._map.set_dirstate_item(filename, entry)
610 new = True
611 else:
612 # XXX This is probably overkill for more case, but we need this to
613 # fully replace the `normallookup` call with `set_tracked` one.
614 # Consider smoothing this in the future.
615 self.set_possibly_dirty(filename)
616 return new
617
618 def set_untracked(self, f):
619 """Mark a file as no longer tracked in the dirstate map"""
620 # in merge is only trigger more logic, so it "fine" to pass it.
621 #
622 # the inner rust dirstate map code need to be adjusted once the API
623 # for dirstate/dirstatemap/DirstateItem is a bit more settled
624 entry = self.get(f)
625 if entry is None:
626 return False
627 else:
628 if entry.added:
629 self._map.drop_item_and_copy_source(f)
630 else:
631 self._map.removefile(f, in_merge=True)
632 return True
633
634 def removefile(self, *args, **kwargs):
635 return self._map.removefile(*args, **kwargs)
636 544
637 @property 545 @property
638 def copymap(self): 546 def copymap(self):
639 return self._map.copymap() 547 return self._map.copymap()
640 548
658 util.clearcachedproperty(self, b"dirfoldmap") 566 util.clearcachedproperty(self, b"dirfoldmap")
659 567
660 def items(self): 568 def items(self):
661 return self._map.items() 569 return self._map.items()
662 570
571 # forward for python2,3 compat
572 iteritems = items
573
663 def keys(self): 574 def keys(self):
664 return iter(self._map) 575 return iter(self._map)
665 576
666 # forward for python2,3 compat 577 ### reading/setting parents
667 iteritems = items
668
669 def _opendirstatefile(self):
670 fp, mode = txnutil.trypending(
671 self._root, self._opener, self._filename
672 )
673 if self._pendingmode is not None and self._pendingmode != mode:
674 fp.close()
675 raise error.Abort(
676 _(b'working directory state may be changed parallelly')
677 )
678 self._pendingmode = mode
679 return fp
680
681 def _readdirstatefile(self, size=-1):
682 try:
683 with self._opendirstatefile() as fp:
684 return fp.read(size)
685 except IOError as err:
686 if err.errno != errno.ENOENT:
687 raise
688 # File doesn't exist, so the current state is empty
689 return b''
690 578
691 def setparents(self, p1, p2, fold_p2=False): 579 def setparents(self, p1, p2, fold_p2=False):
692 self._parents = (p1, p2) 580 self._parents = (p1, p2)
693 self._dirtyparents = True 581 self._dirtyparents = True
694 copies = {} 582 copies = {}
750 _(b'working directory state appears damaged!') 638 _(b'working directory state appears damaged!')
751 ) 639 )
752 640
753 return self._parents 641 return self._parents
754 642
755 @property 643 ### disk interaction
756 def docket(self):
757 if not self._docket:
758 if not self._use_dirstate_v2:
759 raise error.ProgrammingError(
760 b'dirstate only has a docket in v2 format'
761 )
762 self._docket = docketmod.DirstateDocket.parse(
763 self._readdirstatefile(), self._nodeconstants
764 )
765 return self._docket
766 644
767 @propertycache 645 @propertycache
768 def _map(self): 646 def identity(self):
769 """ 647 self._map
770 Fills the Dirstatemap when called. 648 return self.identity
771 """
772 # ignore HG_PENDING because identity is used only for writing
773 self.identity = util.filestat.frompath(
774 self._opener.join(self._filename)
775 )
776
777 if self._use_dirstate_v2:
778 if self.docket.uuid:
779 # TODO: use mmap when possible
780 data = self._opener.read(self.docket.data_filename())
781 else:
782 data = b''
783 self._map = rustmod.DirstateMap.new_v2(
784 data, self.docket.data_size, self.docket.tree_metadata
785 )
786 parents = self.docket.parents
787 else:
788 self._map, parents = rustmod.DirstateMap.new_v1(
789 self._readdirstatefile()
790 )
791
792 if parents and not self._dirtyparents:
793 self.setparents(*parents)
794
795 self.__contains__ = self._map.__contains__
796 self.__getitem__ = self._map.__getitem__
797 self.get = self._map.get
798 return self._map
799 649
800 def write(self, tr, st, now): 650 def write(self, tr, st, now):
801 if not self._use_dirstate_v2: 651 if not self._use_dirstate_v2:
802 p1, p2 = self.parents() 652 p1, p2 = self.parents()
803 packed = self._map.write_v1(p1, p2, now) 653 packed = self._map.write_v1(p1, p2, now)
852 self._docket = new_docket 702 self._docket = new_docket
853 # Reload from the newly-written file 703 # Reload from the newly-written file
854 util.clearcachedproperty(self, b"_map") 704 util.clearcachedproperty(self, b"_map")
855 self._dirtyparents = False 705 self._dirtyparents = False
856 706
707 def _opendirstatefile(self):
708 fp, mode = txnutil.trypending(
709 self._root, self._opener, self._filename
710 )
711 if self._pendingmode is not None and self._pendingmode != mode:
712 fp.close()
713 raise error.Abort(
714 _(b'working directory state may be changed parallelly')
715 )
716 self._pendingmode = mode
717 return fp
718
719 def _readdirstatefile(self, size=-1):
720 try:
721 with self._opendirstatefile() as fp:
722 return fp.read(size)
723 except IOError as err:
724 if err.errno != errno.ENOENT:
725 raise
726 # File doesn't exist, so the current state is empty
727 return b''
728
729 ### code related to maintaining and accessing "extra" property
730 # (e.g. "has_dir")
731
857 @propertycache 732 @propertycache
858 def filefoldmap(self): 733 def filefoldmap(self):
859 """Returns a dictionary mapping normalized case paths to their 734 """Returns a dictionary mapping normalized case paths to their
860 non-normalized versions. 735 non-normalized versions.
861 """ 736 """
864 def hastrackeddir(self, d): 739 def hastrackeddir(self, d):
865 return self._map.hastrackeddir(d) 740 return self._map.hastrackeddir(d)
866 741
867 def hasdir(self, d): 742 def hasdir(self, d):
868 return self._map.hasdir(d) 743 return self._map.hasdir(d)
869
870 @propertycache
871 def identity(self):
872 self._map
873 return self.identity
874 744
875 @propertycache 745 @propertycache
876 def dirfoldmap(self): 746 def dirfoldmap(self):
877 f = {} 747 f = {}
878 normcase = util.normcase 748 normcase = util.normcase
879 for name in self._map.tracked_dirs(): 749 for name in self._map.tracked_dirs():
880 f[normcase(name)] = name 750 f[normcase(name)] = name
881 return f 751 return f
752
753 ### code related to manipulation of entries and copy-sources
882 754
883 def set_possibly_dirty(self, filename): 755 def set_possibly_dirty(self, filename):
884 """record that the current state of the file on disk is unknown""" 756 """record that the current state of the file on disk is unknown"""
885 entry = self[filename] 757 entry = self[filename]
886 entry.set_possibly_dirty() 758 entry.set_possibly_dirty()
896 self._map.copymap().pop(filename, None) 768 self._map.copymap().pop(filename, None)
897 769
898 def __setitem__(self, key, value): 770 def __setitem__(self, key, value):
899 assert isinstance(value, DirstateItem) 771 assert isinstance(value, DirstateItem)
900 self._map.set_dirstate_item(key, value) 772 self._map.set_dirstate_item(key, value)
773
774 def reset_state(
775 self,
776 filename,
777 wc_tracked=False,
778 p1_tracked=False,
779 p2_tracked=False,
780 merged=False,
781 clean_p1=False,
782 clean_p2=False,
783 possibly_dirty=False,
784 parentfiledata=None,
785 ):
786 """Set a entry to a given state, disregarding all previous state
787
788 This is to be used by the part of the dirstate API dedicated to
789 adjusting the dirstate after a update/merge.
790
791 note: calling this might result to no entry existing at all if the
792 dirstate map does not see any point at having one for this file
793 anymore.
794 """
795 if merged and (clean_p1 or clean_p2):
796 msg = (
797 b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
798 )
799 raise error.ProgrammingError(msg)
800 # copy information are now outdated
801 # (maybe new information should be in directly passed to this function)
802 self.copymap.pop(filename, None)
803
804 if not (p1_tracked or p2_tracked or wc_tracked):
805 self._map.drop_item_and_copy_source(filename)
806 elif merged:
807 # XXX might be merged and removed ?
808 entry = self.get(filename)
809 if entry is not None and entry.tracked:
810 # XXX mostly replicate dirstate.other parent. We should get
811 # the higher layer to pass us more reliable data where `merged`
812 # actually mean merged. Dropping the else clause will show
813 # failure in `test-graft.t`
814 self.addfile(filename, merged=True)
815 else:
816 self.addfile(filename, from_p2=True)
817 elif not (p1_tracked or p2_tracked) and wc_tracked:
818 self.addfile(
819 filename, added=True, possibly_dirty=possibly_dirty
820 )
821 elif (p1_tracked or p2_tracked) and not wc_tracked:
822 # XXX might be merged and removed ?
823 self[filename] = DirstateItem.from_v1_data(b'r', 0, 0, 0)
824 elif clean_p2 and wc_tracked:
825 if p1_tracked or self.get(filename) is not None:
826 # XXX the `self.get` call is catching some case in
827 # `test-merge-remove.t` where the file is tracked in p1, the
828 # p1_tracked argument is False.
829 #
830 # In addition, this seems to be a case where the file is marked
831 # as merged without actually being the result of a merge
832 # action. So thing are not ideal here.
833 self.addfile(filename, merged=True)
834 else:
835 self.addfile(filename, from_p2=True)
836 elif not p1_tracked and p2_tracked and wc_tracked:
837 self.addfile(
838 filename, from_p2=True, possibly_dirty=possibly_dirty
839 )
840 elif possibly_dirty:
841 self.addfile(filename, possibly_dirty=possibly_dirty)
842 elif wc_tracked:
843 # this is a "normal" file
844 if parentfiledata is None:
845 msg = b'failed to pass parentfiledata for a normal file: %s'
846 msg %= filename
847 raise error.ProgrammingError(msg)
848 mode, size, mtime = parentfiledata
849 self.addfile(filename, mode=mode, size=size, mtime=mtime)
850 else:
851 assert False, 'unreachable'
852
853 def set_tracked(self, filename):
854 new = False
855 entry = self.get(filename)
856 if entry is None:
857 self.addfile(filename, added=True)
858 new = True
859 elif not entry.tracked:
860 entry.set_tracked()
861 self._map.set_dirstate_item(filename, entry)
862 new = True
863 else:
864 # XXX This is probably overkill for more case, but we need this to
865 # fully replace the `normallookup` call with `set_tracked` one.
866 # Consider smoothing this in the future.
867 self.set_possibly_dirty(filename)
868 return new
869
870 def set_untracked(self, f):
871 """Mark a file as no longer tracked in the dirstate map"""
872 # in merge is only trigger more logic, so it "fine" to pass it.
873 #
874 # the inner rust dirstate map code need to be adjusted once the API
875 # for dirstate/dirstatemap/DirstateItem is a bit more settled
876 entry = self.get(f)
877 if entry is None:
878 return False
879 else:
880 if entry.added:
881 self._map.drop_item_and_copy_source(f)
882 else:
883 self._map.removefile(f, in_merge=True)
884 return True
885
886 ### Legacy method we need to get rid of
887
888 def addfile(
889 self,
890 f,
891 mode=0,
892 size=None,
893 mtime=None,
894 added=False,
895 merged=False,
896 from_p2=False,
897 possibly_dirty=False,
898 ):
899 if added:
900 assert not possibly_dirty
901 assert not from_p2
902 item = DirstateItem.new_added()
903 elif merged:
904 assert not possibly_dirty
905 assert not from_p2
906 item = DirstateItem.new_merged()
907 elif from_p2:
908 assert not possibly_dirty
909 item = DirstateItem.new_from_p2()
910 elif possibly_dirty:
911 item = DirstateItem.new_possibly_dirty()
912 else:
913 assert size is not None
914 assert mtime is not None
915 size = size & rangemask
916 mtime = mtime & rangemask
917 item = DirstateItem.new_normal(mode, size, mtime)
918 self._map.addfile(f, item)
919 if added:
920 self.copymap.pop(f, None)
921
922 def removefile(self, *args, **kwargs):
923 return self._map.removefile(*args, **kwargs)