# HG changeset patch
# User Matt Harbison <matt_harbison@yahoo.com>
# Date 1736789073 18000
#      Mon Jan 13 12:24:33 2025 -0500
# Node ID f3762eafed66e8d202aecaba4d77c1f74cf55bd7
# Parent  10e7adbffa8c800c80ca1514095ad0e54e192afb
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.

diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -11,7 +11,7 @@
 import os
 import struct
 import typing
-from typing import Dict, Iterator, Optional, Tuple
+from typing import Dict, Iterable, Iterator, Optional, Tuple
 
 from .i18n import _
 from .node import nullrev
@@ -41,6 +41,18 @@
     worker,
 )
 
+if typing.TYPE_CHECKING:
+    # TODO: figure out what exactly is in this tuple
+    MergeResultData = tuple
+    MergeResultAction = tuple[bytes, Optional[MergeResultData], bytes]
+    """The filename, data about the merge, and message about the merge."""
+
+    FileMappingValue = tuple[
+        mergestatemod.MergeAction, Optional[MergeResultData], bytes
+    ]
+    """The merge action, data about the merge, and message about the merge, for
+    the keyed file."""
+
 rust_update_mod = policy.importrust("update")
 
 _pack = struct.pack
@@ -132,7 +144,9 @@
         return None
 
 
-def _checkunknownfiles(repo, wctx, mctx, force, mresult, mergeforce):
+def _checkunknownfiles(
+    repo, wctx, mctx, force, mresult: mergeresult, mergeforce
+):
     """
     Considers any actions that care about the presence of conflicting unknown
     files. For some actions, the result is to abort; for others, it is to
@@ -277,7 +291,7 @@
     )
 
 
-def _forgetremoved(wctx, mctx, branchmerge, mresult):
+def _forgetremoved(wctx, mctx, branchmerge, mresult: mergeresult) -> None:
     """
     Forget removed files
 
@@ -310,7 +324,7 @@
                 )
 
 
-def _checkcollision(repo, wmf, mresult):
+def _checkcollision(repo, wmf, mresult: mergeresult | None) -> None:
     """
     Check for case-folding collisions.
     """
@@ -378,7 +392,7 @@
         lastfull = f
 
 
-def _filesindirs(repo, manifest, dirs):
+def _filesindirs(repo, manifest, dirs) -> Iterator[tuple[bytes, bytes]]:
     """
     Generator that yields pairs of all the files in the manifest that are found
     inside the directories listed in dirs, and which directory they are found
@@ -391,7 +405,7 @@
                 break
 
 
-def checkpathconflicts(repo, wctx, mctx, mresult):
+def checkpathconflicts(repo, wctx, mctx, mresult: mergeresult) -> None:
     """
     Check if any actions introduce path conflicts in the repository, updating
     actions to record or handle the path conflict accordingly.
@@ -492,7 +506,13 @@
         ctxname = bytes(mctx).rstrip(b'+')
         for f, p in _filesindirs(repo, mf, remoteconflicts):
             if f not in deletedfiles:
-                m, args, msg = mresult.getfile(p)
+                mapping_value = mresult.getfile(p)
+
+                # Help pytype- in theory, this could be None since no default
+                # value is passed to getfile() above.
+                assert mapping_value is not None
+
+                m, args, msg = mapping_value
                 pnew = util.safename(p, ctxname, wctx, set(mresult.files()))
                 if m in (
                     mergestatemod.ACTION_DELETED_CHANGED,
@@ -526,7 +546,9 @@
         )
 
 
-def _filternarrowactions(narrowmatch, branchmerge, mresult):
+def _filternarrowactions(
+    narrowmatch, branchmerge, mresult: mergeresult
+) -> None:
     """
     Filters out actions that can ignored because the repo is narrowed.
 
@@ -567,7 +589,12 @@
     It has information about what actions need to be performed on dirstate
     mapping of divergent renames and other such cases."""
 
-    def __init__(self):
+    _filemapping: dict[bytes, FileMappingValue]
+    _actionmapping: dict[
+        mergestatemod.MergeAction, dict[bytes, tuple[MergeResultData, bytes]]
+    ]
+
+    def __init__(self) -> None:
         """
         filemapping: dict of filename as keys and action related info as values
         diverge: mapping of source name -> list of dest name for
@@ -589,7 +616,13 @@
         self._diverge = diverge
         self._renamedelete = renamedelete
 
-    def addfile(self, filename, action, data, message):
+    def addfile(
+        self,
+        filename: bytes,
+        action: mergestatemod.MergeAction,
+        data: MergeResultData | None,
+        message,
+    ) -> None:
         """adds a new file to the mergeresult object
 
         filename: file which we are adding
@@ -606,7 +639,12 @@
         self._filemapping[filename] = (action, data, message)
         self._actionmapping[action][filename] = (data, message)
 
-    def mapaction(self, actionfrom, actionto, transform):
+    def mapaction(
+        self,
+        actionfrom: mergestatemod.MergeAction,
+        actionto: mergestatemod.MergeAction,
+        transform,
+    ):
         """changes all occurrences of action `actionfrom` into `actionto`,
         transforming its args with the function `transform`.
         """
@@ -618,7 +656,9 @@
             self._filemapping[f] = (actionto, data, msg)
             dest[f] = (data, msg)
 
-    def getfile(self, filename, default_return=None):
+    def getfile(
+        self, filename: bytes, default_return: FileMappingValue | None = None
+    ) -> FileMappingValue | None:
         """returns (action, args, msg) about this file
 
         returns default_return if the file is not present"""
@@ -626,7 +666,9 @@
             return self._filemapping[filename]
         return default_return
 
-    def files(self, actions=None):
+    def files(
+        self, actions: Iterable[mergestatemod.MergeAction] | None = None
+    ) -> Iterator[bytes]:
         """returns files on which provided action needs to perfromed
 
         If actions is None, all files are returned
@@ -640,14 +682,16 @@
             for a in actions:
                 yield from self._actionmapping[a]
 
-    def removefile(self, filename):
+    def removefile(self, filename: bytes) -> None:
         """removes a file from the mergeresult object as the file might
         not merging anymore"""
         action, data, message = self._filemapping[filename]
         del self._filemapping[filename]
         del self._actionmapping[action][filename]
 
-    def getactions(self, actions, sort=False):
+    def getactions(
+        self, actions: Iterable[mergestatemod.MergeAction], sort: bool = False
+    ) -> Iterator[MergeResultAction]:
         """get list of files which are marked with these actions
         if sort is true, files for each action is sorted and then added
 
@@ -662,7 +706,9 @@
                 for f, (args, msg) in self._actionmapping[a].items():
                     yield f, args, msg
 
-    def len(self, actions=None):
+    def len(
+        self, actions: Iterable[mergestatemod.MergeAction] | None = None
+    ) -> int:
         """returns number of files which needs actions
 
         if actions is passed, total of number of files in that action
@@ -673,13 +719,15 @@
 
         return sum(len(self._actionmapping[a]) for a in actions)
 
-    def filemap(self, sort=False) -> Iterator[tuple]:  # TODO: fill out tuple
+    def filemap(
+        self, sort: bool = False
+    ) -> Iterator[tuple[bytes, MergeResultData]]:
         if sort:
             yield from sorted(self._filemapping.items())
         else:
             yield from self._filemapping.items()
 
-    def addcommitinfo(self, filename, key, value):
+    def addcommitinfo(self, filename: bytes, key, value) -> None:
         """adds key-value information about filename which will be required
         while committing this merge"""
         self._commitinfo[filename][key] = value
@@ -697,7 +745,9 @@
         return self._commitinfo
 
     @property
-    def actionsdict(self):
+    def actionsdict(
+        self,
+    ) -> dict[mergestatemod.MergeAction, list[MergeResultAction]]:
         """returns a dictionary of actions to be perfomed with action as key
         and a list of files and related arguments as values"""
         res = collections.defaultdict(list)
@@ -706,13 +756,13 @@
                 res[a].append((f, args, msg))
         return res
 
-    def setactions(self, actions):
+    def setactions(self, actions) -> None:
         self._filemapping = actions
         self._actionmapping = collections.defaultdict(dict)
         for f, (act, data, msg) in self._filemapping.items():
             self._actionmapping[act][f] = data, msg
 
-    def hasconflicts(self):
+    def hasconflicts(self) -> bool:
         """tells whether this merge resulted in some actions which can
         result in conflicts or not"""
         for a in self._actionmapping.keys():
@@ -743,7 +793,7 @@
     acceptremote,
     followcopies,
     forcefulldiff=False,
-):
+) -> mergeresult:
     """
     Merge wctx and p2 with ancestor pa and generate merge action list
 
@@ -1117,7 +1167,7 @@
     return mresult
 
 
-def _resolvetrivial(repo, wctx, mctx, ancestor, mresult):
+def _resolvetrivial(repo, wctx, mctx, ancestor, mresult: mergeresult) -> None:
     """Resolves false conflicts where the nodeid changed but the content
     remained the same."""
     # We force a copy of actions.items() because we're going to mutate
@@ -1146,7 +1196,7 @@
     followcopies,
     matcher=None,
     mergeforce=False,
-):
+) -> mergeresult:
     """
     Calculate the actions needed to merge mctx into wctx using ancestors
 
@@ -1469,7 +1519,7 @@
     yield True, filedata
 
 
-def _prefetchfiles(repo, ctx, mresult):
+def _prefetchfiles(repo, ctx, mresult: mergeresult) -> None:
     """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
     of merge actions.  ``ctx`` is the context being merged in."""
 
@@ -1516,7 +1566,7 @@
 
 def applyupdates(
     repo,
-    mresult,
+    mresult: mergeresult,
     wctx,
     mctx,
     overwrite,
diff --git a/mercurial/sparse.py b/mercurial/sparse.py
--- a/mercurial/sparse.py
+++ b/mercurial/sparse.py
@@ -548,7 +548,7 @@
         elif (old and not new) or (not old and not new and file in dirstate):
             dropped.append(file)
             if file not in pending:
-                mresult.addfile(file, mergestatemod.ACTION_REMOVE, [], b'')
+                mresult.addfile(file, mergestatemod.ACTION_REMOVE, None, b'')
 
     # Verify there are no pending changes in newly included files
     abort = False