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)