repohooks: propagate yes argument for auto-fixups

Allow main() and direct_main() in pre-upload.py to accept the yes
argument/option. Propagate it through the calling stack
(callable_builtin_hooks -> HookOptions -> check_alint -> fixup_cmd) and
to _attempt_fixes to bypass interactive confirmation prompts.

Bug: 498893733
Test: python3 -m pytest rh/hooks_unittest.py pre-upload_unittest.py
Flag: NONE developer tool change

Change-Id: I684e321768798a6c93c024e893e8a8e8d59dd891
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repohooks/+/606141
Tested-by: Rahul Yadav <yadavrah@google.com>
Commit-Queue: Rahul Yadav <yadavrah@google.com>
Reviewed-by: Sam Saccone <samccone@google.com>
diff --git a/pre-upload.py b/pre-upload.py
index 5c566c6..66dd0d8 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -283,7 +283,9 @@
     return rh.config.PreUploadSettings(paths=paths, global_paths=global_paths)
 
 
-def _attempt_fixes(projects_results: List[rh.results.ProjectResults]) -> None:
+def _attempt_fixes(
+    projects_results: List[rh.results.ProjectResults], yes: bool = False
+) -> None:
     """Attempts to fix fixable results."""
     # Filter out any result that has a fixup.
     fixups: List[Tuple[str, rh.results.HookResult]] = []
@@ -303,7 +305,9 @@
     # If there's more than one fixup available, ask if they want to blindly run
     # them all, or prompt for them one-by-one.
     mode = "some"
-    if len(fixups) > 1:
+    if yes:
+        mode = "all"
+    elif len(fixups) > 1:
         while True:
             response = rh.terminal.str_prompt(
                 "What would you like to do",
@@ -564,6 +568,7 @@
     jobs: Optional[int] = None,
     from_git: bool = False,
     commit_list: Optional[List[str]] = None,
+    yes: bool = False,
 ) -> bool:
     """Run all the hooks
 
@@ -576,6 +581,7 @@
         commit_list: A list of commits to run hooks against.  If None or empty
             list then we'll automatically get the list of commits that would be
             uploaded.
+        yes: Answer yes to all safe prompts.
 
     Returns:
         True if everything passed, else False.
@@ -596,11 +602,11 @@
             # very minimal, so we don't add it then.
             print("", file=sys.stderr)
 
-    _attempt_fixes(results)
+    _attempt_fixes(results, yes=yes)
     return not any(results)
 
 
-def main(project_list, worktree_list=None, **_kwargs):
+def main(project_list, worktree_list=None, yes=False, **_kwargs):
     """Main function invoked directly by repo.
 
     We must use the name "main" as that is what repo requires.
@@ -614,11 +620,12 @@
             project_list, so that each entry in project_list matches with a
             directory in worktree_list.  If None, we will attempt to calculate
             the directories automatically.
+        yes: Answer yes to all safe prompts.
         kwargs: Leave this here for forward-compatibility.
     """
     if not worktree_list:
         worktree_list = [None] * len(project_list)
-    if not _run_projects_hooks(project_list, worktree_list):
+    if not _run_projects_hooks(project_list, worktree_list, yes=yes):
         color = rh.terminal.Color()
         print(
             color.color(color.RED, "FATAL")
@@ -704,6 +711,12 @@
         "automatically chooses an appropriate number for the "
         "current system.",
     )
+    parser.add_argument(
+        "-y",
+        "--yes",
+        action="store_true",
+        help="Answer yes to all safe prompts",
+    )
     parser.add_argument("commits", nargs="*", help="Check specific commits")
     opts = parser.parse_args(argv)
 
@@ -734,6 +747,7 @@
             jobs=opts.jobs,
             from_git=opts.git,
             commit_list=opts.commits,
+            yes=opts.yes,
         ):
             return 0
     except KeyboardInterrupt:
diff --git a/pre-upload_unittest.py b/pre-upload_unittest.py
new file mode 100755
index 0000000..6591e5f
--- /dev/null
+++ b/pre-upload_unittest.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+# Copyright 2026 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unittests for pre-upload.py."""
+
+import importlib.util
+import os
+from pathlib import Path
+import sys
+import unittest
+from unittest import mock
+
+
+# Set up paths so we can import rh modules.
+THIS_FILE = Path(__file__).resolve()
+THIS_DIR = THIS_FILE.parent
+sys.path.insert(0, str(THIS_DIR))
+
+# pylint: disable=wrong-import-position
+import rh.utils  # noqa: E402
+
+
+# pylint: enable=wrong-import-position
+
+
+# Load pre-upload.py as a module (since its filename has a hyphen).
+spec = importlib.util.spec_from_file_location(
+    "pre_upload", os.path.join(THIS_DIR, "pre-upload.py")
+)
+pre_upload = importlib.util.module_from_spec(spec)
+sys.modules["pre_upload"] = pre_upload
+spec.loader.exec_module(pre_upload)
+
+
+# pylint: disable=protected-access
+class PreUploadMainTests(unittest.TestCase):
+    """Test pre-upload.py main/direct_main parameter propagation."""
+
+    @mock.patch("pre_upload._run_projects_hooks", return_value=True)
+    def test_main_yes_propagation(self, mock_run):
+        """Verify main(..., yes=True) passes yes=True to _run_projects_hooks."""
+        pre_upload.main(["project"], yes=True)
+        mock_run.assert_called_once_with(["project"], [None], yes=True)
+
+    @mock.patch("pre_upload._run_project_hooks", return_value=True)
+    @mock.patch("pre_upload._attempt_fixes")
+    def test_run_projects_hooks_yes_propagation(self, mock_attempt, mock_run):
+        """Verify _run_projects_hooks passes yes=True to _attempt_fixes."""
+        pre_upload._run_projects_hooks(["p1"], [None], yes=True)
+        mock_run.assert_called_once_with(
+            "p1",
+            proj_dir=None,
+            jobs=None,
+            from_git=False,
+            commit_list=None,
+        )
+        mock_attempt.assert_called_once_with([True], yes=True)
+
+    @mock.patch("pre_upload._run_projects_hooks", return_value=True)
+    @mock.patch("pre_upload._identify_project", return_value="project")
+    @mock.patch("rh.git.is_git_repository", return_value=True)
+    @mock.patch("rh.utils.run")
+    def test_direct_main_yes(
+        self, mock_run_cmd, _mock_is_git, _mock_identify, mock_run
+    ):
+        """Verify direct_main with --yes passes yes=True to
+        _run_projects_hooks.
+        """
+        mock_run_cmd.return_value = rh.utils.CompletedProcess(
+            stdout="/path/to/repo/.git\n"
+        )
+        pre_upload.direct_main(["--yes", "commit_hash"])
+        mock_run.assert_called_once_with(
+            ["project"],
+            ["/path/to/repo"],
+            jobs=None,
+            from_git=False,
+            commit_list=["commit_hash"],
+            yes=True,
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/rh/hooks.py b/rh/hooks.py
index 7bdb8b1..2397745 100644
--- a/rh/hooks.py
+++ b/rh/hooks.py
@@ -1355,7 +1355,7 @@
     head_hash = rh.git.get_commit_for_ref("HEAD")
     is_head = commit in ("HEAD", head_hash)
     fixup_cmd = (
-        [alint_path, "fix", "--no_amend", "--commit", commit]
+        [alint_path, "fix", "-y", "--no_amend", "--commit", commit]
         if is_head and result.returncode in (5, 6)
         else None
     )
diff --git a/rh/hooks_unittest.py b/rh/hooks_unittest.py
index 1403a31..09b5e7c 100755
--- a/rh/hooks_unittest.py
+++ b/rh/hooks_unittest.py
@@ -1274,7 +1274,7 @@
             self.assertIsNotNone(ret)
             self.assertEqual(
                 ret[0].fixup_cmd,
-                ["alint", "fix", "--no_amend", "--commit", commit],
+                ["alint", "fix", "-y", "--no_amend", "--commit", commit],
             )
             self.assertFalse(ret[0].is_warning())
             self.assertEqual(ret[0].result.returncode, 5)
@@ -1287,7 +1287,7 @@
             self.assertIsNotNone(ret)
             self.assertEqual(
                 ret[0].fixup_cmd,
-                ["alint", "fix", "--no_amend", "--commit", commit],
+                ["alint", "fix", "-y", "--no_amend", "--commit", commit],
             )
             self.assertTrue(ret[0].is_warning())
             self.assertEqual(ret[0].result.returncode, 6)