Skip to content

Commit e6ad708

Browse files
Rahul Yadavgerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
authored andcommitted
hooks: add --fix option to auto-apply hook fixes
Pass the --fix flag as a keyword argument "fix" to the hook main function. This allows hooks (such as git-repohooks) to decouple automated fix application from the -y/--yes flag so that -y can answer yes to upload confirmation prompts without triggering file mutations. Companion change in git-repohooks: https://gerrit-review.googlesource.com/c/git-repohooks/+/621761 Bug: 546510319 Test: python3 -m pytest tests/test_hooks.py Change-Id: If0288d4791fc0a2aba6e88854e3aa81b0923b664 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/619281 Commit-Queue: Rahul Yadav <yadavrah@google.com> Tested-by: Rahul Yadav <yadavrah@google.com> Reviewed-by: Gavin Mak <gavinmak@google.com>
1 parent 09914bc commit e6ad708

6 files changed

Lines changed: 98 additions & 7 deletions

File tree

completion.zsh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ _repo() {
400400
'--no-verify[Do not verify]' \
401401
'--verify[Verify]' \
402402
'--ignore-hooks[Ignore hooks]' \
403+
'--fix[Automatically fix]' \
403404
'*: :->project'
404405
;;
405406
version)

docs/repo-hooks.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,14 @@ be useful when deploying automatic fixes.
8888
If the repo command that triggered the hook supports a "yes" option (e.g.,
8989
`repo upload --yes`), this option is propagated to the hook's `main` function
9090
as `yes` parameter (defaulting to `False`). Hooks can use this to bypass
91-
interactive confirmation prompts when they can automatically fix issues.
91+
interactive confirmation prompts for safe non-modifying operations.
92+
93+
### Automated Fixes
94+
95+
If the repo command that triggered the hook supports a "fix" option (e.g.,
96+
`repo upload --fix`), this option is propagated to the hook's `main` function
97+
as `fix` parameter (defaulting to `False`). Hooks can use this to automatically
98+
apply fixes without prompting the user.
9299

93100
### Shebang Handling
94101

@@ -126,7 +133,7 @@ This hook runs when people run `repo upload`.
126133
The `pre-upload.py` file should be defined like:
127134

128135
```py
129-
def main(project_list, worktree_list=None, yes=False, **kwargs):
136+
def main(project_list, worktree_list=None, fix=False, yes=False, **kwargs):
130137
"""Main function invoked directly by repo.
131138
132139
We must use the name "main" as that is what repo requires.
@@ -137,6 +144,7 @@ def main(project_list, worktree_list=None, yes=False, **kwargs):
137144
project_list, so that each entry in project_list matches with a
138145
directory in worktree_list. If None, we will attempt to calculate
139146
the directories automatically.
147+
fix: Whether to automatically apply fixes without prompting.
140148
yes: Whether to answer yes to all safe prompts (see
141149
[Safe Prompts](#safe-prompts)).
142150
kwargs: Leave this here for forward-compatibility.

hooks.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import optparse
1516
import os
1617
import re
1718
import sys
@@ -69,6 +70,7 @@ def __init__(
6970
ignore_hooks=False,
7071
abort_if_user_denies=False,
7172
yes=False,
73+
fix=False,
7274
):
7375
"""RepoHook constructor.
7476
@@ -91,6 +93,7 @@ def __init__(
9193
abort_if_user_denies: If True, we'll abort running the hook if the
9294
user doesn't allow us to run the hook.
9395
yes: If True, then 'Yes' is assumed for any prompts.
96+
fix: If True, then 'Fix' is assumed for any fixup prompts.
9497
"""
9598
self._hook_type = hook_type
9699
self._hooks_project = hooks_project
@@ -102,6 +105,7 @@ def __init__(
102105
self._ignore_hooks = ignore_hooks
103106
self._abort_if_user_denies = abort_if_user_denies
104107
self._yes = yes
108+
self._fix = fix
105109

106110
# Store the full path to the script for convenience.
107111
self._script_fullpath = None
@@ -380,6 +384,7 @@ def _ExecuteHook(self, **kwargs):
380384
kwargs = {
381385
**kwargs,
382386
"hook_should_take_kwargs": True,
387+
"fix": self._fix,
383388
"yes": self._yes,
384389
}
385390

@@ -504,12 +509,17 @@ def FromSubcmd(cls, manifest, opt, *args, **kwargs):
504509
).url,
505510
"bug_url": manifest.contactinfo.bugurl,
506511
"yes": getattr(opt, "yes", False),
512+
"fix": getattr(opt, "fix", False),
507513
}
508514
)
509515
return cls(*args, **kwargs)
510516

511517
@staticmethod
512-
def AddOptionGroup(parser, name):
518+
def AddOptionGroup(
519+
parser: optparse.OptionParser,
520+
name: str,
521+
allow_fix: bool = False,
522+
) -> None:
513523
"""Help options relating to the various hooks."""
514524

515525
# Note that verify and no-verify are NOT opposites of each other, which
@@ -533,3 +543,10 @@ def AddOptionGroup(parser, name):
533543
action="store_true",
534544
help="Do not abort if %s hooks fail." % name,
535545
)
546+
if allow_fix:
547+
group.add_option(
548+
"--fix",
549+
action="store_true",
550+
default=False,
551+
help="Automatically apply %s fixes without prompting." % name,
552+
)

man/repo-upload.1

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
2-
.TH REPO "1" "June 2026" "repo upload" "Repo Manual"
2+
.TH REPO "1" "August 2026" "repo upload" "Repo Manual"
33
.SH NAME
44
repo \- repo upload - manual page for repo upload
55
.SH SYNOPSIS
@@ -112,6 +112,9 @@ Run the pre\-upload hook without prompting.
112112
.TP
113113
\fB\-\-ignore\-hooks\fR
114114
Do not abort if pre\-upload hooks fail.
115+
.TP
116+
\fB\-\-fix\fR
117+
Automatically apply pre\-upload fixes without prompting.
115118
.PP
116119
Run `repo help upload` to view the detailed manual.
117120
.SH DETAILS

subcmds/upload.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ def _Options(self, p):
379379
default=True,
380380
help="disable verifying ssl certs (unsafe)",
381381
)
382-
RepoHook.AddOptionGroup(p, "pre-upload")
382+
RepoHook.AddOptionGroup(p, "pre-upload", allow_fix=True)
383383

384384
def _SingleBranch(self, opt, branch, people):
385385
project = branch.project

tests/test_hooks.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Unittests for the hooks.py module."""
1616

1717
from io import StringIO
18+
from pathlib import Path
1819
import sys
1920

2021
import pytest
@@ -108,11 +109,11 @@ def fake_execute(**kw):
108109

109110

110111
@pytest.mark.parametrize("yes_val", (True, False))
111-
def test_repo_upload_yes_arg(tmp_path, yes_val: bool) -> None:
112+
def test_repo_upload_yes_arg(tmp_path: Path, yes_val: bool) -> None:
112113
"""Test that yes is passed in kwargs during hook execution."""
113114

114115
class FakeProject:
115-
def __init__(self, worktree):
116+
def __init__(self, worktree: str) -> None:
116117
self.worktree = worktree
117118
self.enabled_repo_hooks = ["pre-upload"]
118119
self.config = None
@@ -139,3 +140,64 @@ def main(project_list, **kwargs):
139140

140141
assert res is True
141142
assert project_list == [yes_val]
143+
144+
145+
@pytest.mark.parametrize("fix_val", (True, False))
146+
def test_repo_upload_fix_arg(tmp_path: Path, fix_val: bool) -> None:
147+
"""Test that fix is passed in kwargs during hook execution."""
148+
149+
class FakeProject:
150+
def __init__(self, worktree: str) -> None:
151+
self.worktree = worktree
152+
self.enabled_repo_hooks = ["pre-upload"]
153+
self.config = None
154+
155+
hook_file = tmp_path / "pre-upload.py"
156+
157+
hook_content = """
158+
def main(project_list, **kwargs):
159+
project_list.append(kwargs.get("fix"))
160+
"""
161+
hook_file.write_text(hook_content)
162+
163+
hook = hooks.RepoHook(
164+
hook_type="pre-upload",
165+
hooks_project=FakeProject(str(tmp_path)),
166+
repo_topdir=str(tmp_path),
167+
manifest_url="https://gerrit",
168+
allow_all_hooks=True,
169+
fix=fix_val,
170+
)
171+
172+
project_list = []
173+
res = hook.Run(project_list=project_list, worktree_list=[])
174+
175+
assert res is True
176+
assert project_list == [fix_val]
177+
178+
179+
def test_from_subcmd_without_fix_option() -> None:
180+
"""Test that FromSubcmd works when opt does not have fix attribute."""
181+
182+
class Remote:
183+
url = "https://gerrit"
184+
185+
class FakeManifest:
186+
repo_hooks_project = None
187+
topdir = "/fake/topdir"
188+
189+
class manifestProject:
190+
@staticmethod
191+
def GetRemote(name: str) -> "Remote":
192+
return Remote()
193+
194+
class contactinfo:
195+
bugurl = "https://bugs"
196+
197+
class FakeOpt:
198+
bypass_hooks = False
199+
allow_all_hooks = False
200+
ignore_hooks = False
201+
202+
hook = hooks.RepoHook.FromSubcmd(FakeManifest(), FakeOpt(), "post-sync")
203+
assert hook._fix is False

0 commit comments

Comments
 (0)