comparison mercurial/merge.py @ 52697:f3762eafed66

typing: add some type annotations to the `merge.mergeresult` class The generated type annotations around `filemap()` and `files()` were slightly modified by the pyupgrade series culminating in 70a75d379daf. This module is way more complicated than the other changes, but these weren't too bad to figure out. The typing caught a trivial issue in `sparse`- it was passing an empty data list to `addfile()` for the `ACTION_REMOVE` case, instead of a tuple or None. `merge.manifestmerge()` calls this function with None for the data, so 1) it has to be typed as optional, and 2) is safe to pass None in the sparse code.
author Matt Harbison <matt_harbison@yahoo.com>
date Mon, 13 Jan 2025 12:24:33 -0500
parents 45dc0f874b8c
children ea9846b8e539
comparison
equal deleted inserted replaced
52696:10e7adbffa8c 52697:f3762eafed66
9 9
10 import collections 10 import collections
11 import os 11 import os
12 import struct 12 import struct
13 import typing 13 import typing
14 from typing import Dict, Iterator, Optional, Tuple 14 from typing import Dict, Iterable, Iterator, Optional, Tuple
15 15
16 from .i18n import _ 16 from .i18n import _
17 from .node import nullrev 17 from .node import nullrev
18 from .thirdparty import attr 18 from .thirdparty import attr
19 19
39 subrepoutil, 39 subrepoutil,
40 util, 40 util,
41 worker, 41 worker,
42 ) 42 )
43 43
44 if typing.TYPE_CHECKING:
45 # TODO: figure out what exactly is in this tuple
46 MergeResultData = tuple
47 MergeResultAction = tuple[bytes, Optional[MergeResultData], bytes]
48 """The filename, data about the merge, and message about the merge."""
49
50 FileMappingValue = tuple[
51 mergestatemod.MergeAction, Optional[MergeResultData], bytes
52 ]
53 """The merge action, data about the merge, and message about the merge, for
54 the keyed file."""
55
44 rust_update_mod = policy.importrust("update") 56 rust_update_mod = policy.importrust("update")
45 57
46 _pack = struct.pack 58 _pack = struct.pack
47 _unpack = struct.unpack 59 _unpack = struct.unpack
48 60
130 if relf not in repo.dirstate: 142 if relf not in repo.dirstate:
131 return f 143 return f
132 return None 144 return None
133 145
134 146
135 def _checkunknownfiles(repo, wctx, mctx, force, mresult, mergeforce): 147 def _checkunknownfiles(
148 repo, wctx, mctx, force, mresult: mergeresult, mergeforce
149 ):
136 """ 150 """
137 Considers any actions that care about the presence of conflicting unknown 151 Considers any actions that care about the presence of conflicting unknown
138 files. For some actions, the result is to abort; for others, it is to 152 files. For some actions, the result is to abort; for others, it is to
139 choose a different action. 153 choose a different action.
140 """ 154 """
275 mresult.mapaction( 289 mresult.mapaction(
276 mergestatemod.ACTION_CREATED, mergestatemod.ACTION_GET, transformargs 290 mergestatemod.ACTION_CREATED, mergestatemod.ACTION_GET, transformargs
277 ) 291 )
278 292
279 293
280 def _forgetremoved(wctx, mctx, branchmerge, mresult): 294 def _forgetremoved(wctx, mctx, branchmerge, mresult: mergeresult) -> None:
281 """ 295 """
282 Forget removed files 296 Forget removed files
283 297
284 If we're jumping between revisions (as opposed to merging), and if 298 If we're jumping between revisions (as opposed to merging), and if
285 neither the working directory nor the target rev has the file, 299 neither the working directory nor the target rev has the file,
308 None, 322 None,
309 b"forget removed", 323 b"forget removed",
310 ) 324 )
311 325
312 326
313 def _checkcollision(repo, wmf, mresult): 327 def _checkcollision(repo, wmf, mresult: mergeresult | None) -> None:
314 """ 328 """
315 Check for case-folding collisions. 329 Check for case-folding collisions.
316 """ 330 """
317 # If the repo is narrowed, filter out files outside the narrowspec. 331 # If the repo is narrowed, filter out files outside the narrowspec.
318 narrowmatch = repo.narrowmatch() 332 narrowmatch = repo.narrowmatch()
376 foldprefix = fold + b'/' 390 foldprefix = fold + b'/'
377 unfoldprefix = f + b'/' 391 unfoldprefix = f + b'/'
378 lastfull = f 392 lastfull = f
379 393
380 394
381 def _filesindirs(repo, manifest, dirs): 395 def _filesindirs(repo, manifest, dirs) -> Iterator[tuple[bytes, bytes]]:
382 """ 396 """
383 Generator that yields pairs of all the files in the manifest that are found 397 Generator that yields pairs of all the files in the manifest that are found
384 inside the directories listed in dirs, and which directory they are found 398 inside the directories listed in dirs, and which directory they are found
385 in. 399 in.
386 """ 400 """
389 if p in dirs: 403 if p in dirs:
390 yield f, p 404 yield f, p
391 break 405 break
392 406
393 407
394 def checkpathconflicts(repo, wctx, mctx, mresult): 408 def checkpathconflicts(repo, wctx, mctx, mresult: mergeresult) -> None:
395 """ 409 """
396 Check if any actions introduce path conflicts in the repository, updating 410 Check if any actions introduce path conflicts in the repository, updating
397 actions to record or handle the path conflict accordingly. 411 actions to record or handle the path conflict accordingly.
398 """ 412 """
399 mf = wctx.manifest() 413 mf = wctx.manifest()
490 if remoteconflicts: 504 if remoteconflicts:
491 # Check if all files in the conflicting directories have been removed. 505 # Check if all files in the conflicting directories have been removed.
492 ctxname = bytes(mctx).rstrip(b'+') 506 ctxname = bytes(mctx).rstrip(b'+')
493 for f, p in _filesindirs(repo, mf, remoteconflicts): 507 for f, p in _filesindirs(repo, mf, remoteconflicts):
494 if f not in deletedfiles: 508 if f not in deletedfiles:
495 m, args, msg = mresult.getfile(p) 509 mapping_value = mresult.getfile(p)
510
511 # Help pytype- in theory, this could be None since no default
512 # value is passed to getfile() above.
513 assert mapping_value is not None
514
515 m, args, msg = mapping_value
496 pnew = util.safename(p, ctxname, wctx, set(mresult.files())) 516 pnew = util.safename(p, ctxname, wctx, set(mresult.files()))
497 if m in ( 517 if m in (
498 mergestatemod.ACTION_DELETED_CHANGED, 518 mergestatemod.ACTION_DELETED_CHANGED,
499 mergestatemod.ACTION_MERGE, 519 mergestatemod.ACTION_MERGE,
500 ): 520 ):
524 raise error.StateError( 544 raise error.StateError(
525 _(b"destination manifest contains path conflicts") 545 _(b"destination manifest contains path conflicts")
526 ) 546 )
527 547
528 548
529 def _filternarrowactions(narrowmatch, branchmerge, mresult): 549 def _filternarrowactions(
550 narrowmatch, branchmerge, mresult: mergeresult
551 ) -> None:
530 """ 552 """
531 Filters out actions that can ignored because the repo is narrowed. 553 Filters out actions that can ignored because the repo is narrowed.
532 554
533 Raise an exception if the merge cannot be completed because the repo is 555 Raise an exception if the merge cannot be completed because the repo is
534 narrowed. 556 narrowed.
565 """An object representing result of merging manifests. 587 """An object representing result of merging manifests.
566 588
567 It has information about what actions need to be performed on dirstate 589 It has information about what actions need to be performed on dirstate
568 mapping of divergent renames and other such cases.""" 590 mapping of divergent renames and other such cases."""
569 591
570 def __init__(self): 592 _filemapping: dict[bytes, FileMappingValue]
593 _actionmapping: dict[
594 mergestatemod.MergeAction, dict[bytes, tuple[MergeResultData, bytes]]
595 ]
596
597 def __init__(self) -> None:
571 """ 598 """
572 filemapping: dict of filename as keys and action related info as values 599 filemapping: dict of filename as keys and action related info as values
573 diverge: mapping of source name -> list of dest name for 600 diverge: mapping of source name -> list of dest name for
574 divergent renames 601 divergent renames
575 renamedelete: mapping of source name -> list of destinations for files 602 renamedelete: mapping of source name -> list of destinations for files
587 614
588 def updatevalues(self, diverge, renamedelete): 615 def updatevalues(self, diverge, renamedelete):
589 self._diverge = diverge 616 self._diverge = diverge
590 self._renamedelete = renamedelete 617 self._renamedelete = renamedelete
591 618
592 def addfile(self, filename, action, data, message): 619 def addfile(
620 self,
621 filename: bytes,
622 action: mergestatemod.MergeAction,
623 data: MergeResultData | None,
624 message,
625 ) -> None:
593 """adds a new file to the mergeresult object 626 """adds a new file to the mergeresult object
594 627
595 filename: file which we are adding 628 filename: file which we are adding
596 action: one of mergestatemod.ACTION_* 629 action: one of mergestatemod.ACTION_*
597 data: a tuple of information like fctx and ctx related to this merge 630 data: a tuple of information like fctx and ctx related to this merge
604 del self._actionmapping[a][filename] 637 del self._actionmapping[a][filename]
605 638
606 self._filemapping[filename] = (action, data, message) 639 self._filemapping[filename] = (action, data, message)
607 self._actionmapping[action][filename] = (data, message) 640 self._actionmapping[action][filename] = (data, message)
608 641
609 def mapaction(self, actionfrom, actionto, transform): 642 def mapaction(
643 self,
644 actionfrom: mergestatemod.MergeAction,
645 actionto: mergestatemod.MergeAction,
646 transform,
647 ):
610 """changes all occurrences of action `actionfrom` into `actionto`, 648 """changes all occurrences of action `actionfrom` into `actionto`,
611 transforming its args with the function `transform`. 649 transforming its args with the function `transform`.
612 """ 650 """
613 orig = self._actionmapping[actionfrom] 651 orig = self._actionmapping[actionfrom]
614 del self._actionmapping[actionfrom] 652 del self._actionmapping[actionfrom]
616 for f, (data, msg) in orig.items(): 654 for f, (data, msg) in orig.items():
617 data = transform(f, data) 655 data = transform(f, data)
618 self._filemapping[f] = (actionto, data, msg) 656 self._filemapping[f] = (actionto, data, msg)
619 dest[f] = (data, msg) 657 dest[f] = (data, msg)
620 658
621 def getfile(self, filename, default_return=None): 659 def getfile(
660 self, filename: bytes, default_return: FileMappingValue | None = None
661 ) -> FileMappingValue | None:
622 """returns (action, args, msg) about this file 662 """returns (action, args, msg) about this file
623 663
624 returns default_return if the file is not present""" 664 returns default_return if the file is not present"""
625 if filename in self._filemapping: 665 if filename in self._filemapping:
626 return self._filemapping[filename] 666 return self._filemapping[filename]
627 return default_return 667 return default_return
628 668
629 def files(self, actions=None): 669 def files(
670 self, actions: Iterable[mergestatemod.MergeAction] | None = None
671 ) -> Iterator[bytes]:
630 """returns files on which provided action needs to perfromed 672 """returns files on which provided action needs to perfromed
631 673
632 If actions is None, all files are returned 674 If actions is None, all files are returned
633 """ 675 """
634 # TODO: think whether we should return renamedelete and 676 # TODO: think whether we should return renamedelete and
638 680
639 else: 681 else:
640 for a in actions: 682 for a in actions:
641 yield from self._actionmapping[a] 683 yield from self._actionmapping[a]
642 684
643 def removefile(self, filename): 685 def removefile(self, filename: bytes) -> None:
644 """removes a file from the mergeresult object as the file might 686 """removes a file from the mergeresult object as the file might
645 not merging anymore""" 687 not merging anymore"""
646 action, data, message = self._filemapping[filename] 688 action, data, message = self._filemapping[filename]
647 del self._filemapping[filename] 689 del self._filemapping[filename]
648 del self._actionmapping[action][filename] 690 del self._actionmapping[action][filename]
649 691
650 def getactions(self, actions, sort=False): 692 def getactions(
693 self, actions: Iterable[mergestatemod.MergeAction], sort: bool = False
694 ) -> Iterator[MergeResultAction]:
651 """get list of files which are marked with these actions 695 """get list of files which are marked with these actions
652 if sort is true, files for each action is sorted and then added 696 if sort is true, files for each action is sorted and then added
653 697
654 Returns a list of tuple of form (filename, data, message) 698 Returns a list of tuple of form (filename, data, message)
655 """ 699 """
660 yield f, args, msg 704 yield f, args, msg
661 else: 705 else:
662 for f, (args, msg) in self._actionmapping[a].items(): 706 for f, (args, msg) in self._actionmapping[a].items():
663 yield f, args, msg 707 yield f, args, msg
664 708
665 def len(self, actions=None): 709 def len(
710 self, actions: Iterable[mergestatemod.MergeAction] | None = None
711 ) -> int:
666 """returns number of files which needs actions 712 """returns number of files which needs actions
667 713
668 if actions is passed, total of number of files in that action 714 if actions is passed, total of number of files in that action
669 only is returned""" 715 only is returned"""
670 716
671 if actions is None: 717 if actions is None:
672 return len(self._filemapping) 718 return len(self._filemapping)
673 719
674 return sum(len(self._actionmapping[a]) for a in actions) 720 return sum(len(self._actionmapping[a]) for a in actions)
675 721
676 def filemap(self, sort=False) -> Iterator[tuple]: # TODO: fill out tuple 722 def filemap(
723 self, sort: bool = False
724 ) -> Iterator[tuple[bytes, MergeResultData]]:
677 if sort: 725 if sort:
678 yield from sorted(self._filemapping.items()) 726 yield from sorted(self._filemapping.items())
679 else: 727 else:
680 yield from self._filemapping.items() 728 yield from self._filemapping.items()
681 729
682 def addcommitinfo(self, filename, key, value): 730 def addcommitinfo(self, filename: bytes, key, value) -> None:
683 """adds key-value information about filename which will be required 731 """adds key-value information about filename which will be required
684 while committing this merge""" 732 while committing this merge"""
685 self._commitinfo[filename][key] = value 733 self._commitinfo[filename][key] = value
686 734
687 @property 735 @property
695 @property 743 @property
696 def commitinfo(self): 744 def commitinfo(self):
697 return self._commitinfo 745 return self._commitinfo
698 746
699 @property 747 @property
700 def actionsdict(self): 748 def actionsdict(
749 self,
750 ) -> dict[mergestatemod.MergeAction, list[MergeResultAction]]:
701 """returns a dictionary of actions to be perfomed with action as key 751 """returns a dictionary of actions to be perfomed with action as key
702 and a list of files and related arguments as values""" 752 and a list of files and related arguments as values"""
703 res = collections.defaultdict(list) 753 res = collections.defaultdict(list)
704 for a, d in self._actionmapping.items(): 754 for a, d in self._actionmapping.items():
705 for f, (args, msg) in d.items(): 755 for f, (args, msg) in d.items():
706 res[a].append((f, args, msg)) 756 res[a].append((f, args, msg))
707 return res 757 return res
708 758
709 def setactions(self, actions): 759 def setactions(self, actions) -> None:
710 self._filemapping = actions 760 self._filemapping = actions
711 self._actionmapping = collections.defaultdict(dict) 761 self._actionmapping = collections.defaultdict(dict)
712 for f, (act, data, msg) in self._filemapping.items(): 762 for f, (act, data, msg) in self._filemapping.items():
713 self._actionmapping[act][f] = data, msg 763 self._actionmapping[act][f] = data, msg
714 764
715 def hasconflicts(self): 765 def hasconflicts(self) -> bool:
716 """tells whether this merge resulted in some actions which can 766 """tells whether this merge resulted in some actions which can
717 result in conflicts or not""" 767 result in conflicts or not"""
718 for a in self._actionmapping.keys(): 768 for a in self._actionmapping.keys():
719 if ( 769 if (
720 a 770 a
741 force, 791 force,
742 matcher, 792 matcher,
743 acceptremote, 793 acceptremote,
744 followcopies, 794 followcopies,
745 forcefulldiff=False, 795 forcefulldiff=False,
746 ): 796 ) -> mergeresult:
747 """ 797 """
748 Merge wctx and p2 with ancestor pa and generate merge action list 798 Merge wctx and p2 with ancestor pa and generate merge action list
749 799
750 branchmerge and force are as passed in to update 800 branchmerge and force are as passed in to update
751 matcher = matcher to filter file lists 801 matcher = matcher to filter file lists
1115 1165
1116 mresult.updatevalues(diverge, renamedelete) 1166 mresult.updatevalues(diverge, renamedelete)
1117 return mresult 1167 return mresult
1118 1168
1119 1169
1120 def _resolvetrivial(repo, wctx, mctx, ancestor, mresult): 1170 def _resolvetrivial(repo, wctx, mctx, ancestor, mresult: mergeresult) -> None:
1121 """Resolves false conflicts where the nodeid changed but the content 1171 """Resolves false conflicts where the nodeid changed but the content
1122 remained the same.""" 1172 remained the same."""
1123 # We force a copy of actions.items() because we're going to mutate 1173 # We force a copy of actions.items() because we're going to mutate
1124 # actions as we resolve trivial conflicts. 1174 # actions as we resolve trivial conflicts.
1125 for f in list(mresult.files((mergestatemod.ACTION_CHANGED_DELETED,))): 1175 for f in list(mresult.files((mergestatemod.ACTION_CHANGED_DELETED,))):
1144 force, 1194 force,
1145 acceptremote, 1195 acceptremote,
1146 followcopies, 1196 followcopies,
1147 matcher=None, 1197 matcher=None,
1148 mergeforce=False, 1198 mergeforce=False,
1149 ): 1199 ) -> mergeresult:
1150 """ 1200 """
1151 Calculate the actions needed to merge mctx into wctx using ancestors 1201 Calculate the actions needed to merge mctx into wctx using ancestors
1152 1202
1153 Uses manifestmerge() to merge manifest and get list of actions required to 1203 Uses manifestmerge() to merge manifest and get list of actions required to
1154 perform for merging two manifests. If there are multiple ancestors, uses bid 1204 perform for merging two manifests. If there are multiple ancestors, uses bid
1467 if i > 0: 1517 if i > 0:
1468 yield False, (i, f) 1518 yield False, (i, f)
1469 yield True, filedata 1519 yield True, filedata
1470 1520
1471 1521
1472 def _prefetchfiles(repo, ctx, mresult): 1522 def _prefetchfiles(repo, ctx, mresult: mergeresult) -> None:
1473 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict 1523 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
1474 of merge actions. ``ctx`` is the context being merged in.""" 1524 of merge actions. ``ctx`` is the context being merged in."""
1475 1525
1476 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they 1526 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
1477 # don't touch the context to be merged in. 'cd' is skipped, because 1527 # don't touch the context to be merged in. 'cd' is skipped, because
1514 ) 1564 )
1515 1565
1516 1566
1517 def applyupdates( 1567 def applyupdates(
1518 repo, 1568 repo,
1519 mresult, 1569 mresult: mergeresult,
1520 wctx, 1570 wctx,
1521 mctx, 1571 mctx,
1522 overwrite, 1572 overwrite,
1523 wantfiledata, 1573 wantfiledata,
1524 labels=None, 1574 labels=None,