Mercurial > hg
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) |