Mercurial > public > mercurial-scm > hg
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, |