blob: 4976eaf7d4a121a810d1c40066b2ec5ac222e4cc [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Fuchsia platform hook action adapters.
Integrates Fuchsia platform formatting and commit message checking
with the generic git_staging engine.
"""
from __future__ import annotations
import shutil
import sys
from collections.abc import Callable, Sequence
from dataclasses import dataclass, field
from pathlib import Path
from agents.lib import git_staging
from agents.lib.git_staging.engine import run_cmd
from agents.lib.githooks.reporters import ConsoleReporter
from agents.lib.paths import find_fuchsia_dir
DEFAULT_FORMATTABLE_EXTENSIONS: tuple[str, ...] = (
".py",
".md",
".c",
".cc",
".cpp",
".h",
".hh",
".hpp",
".rs",
".go",
".fidl",
".gn",
".gni",
".json",
".json5",
".cml",
".proto",
".ts",
)
@dataclass(frozen=True)
class HookContext(git_staging.ActionContext):
"""Action context for Fuchsia git hooks including agent execution flag."""
is_agent: bool = False
reporter: ConsoleReporter = field(default_factory=ConsoleReporter)
@dataclass(frozen=True)
class HookAction:
"""Action executed on staged or checked repository files.
Attributes:
name: Identifier for the action.
action_fn: Callable[[HookContext, Sequence[str]], bool]
is_mutating: Whether the action modifies files on disk.
extensions: File extensions to filter for (None matches all files).
remediation_cmd_fn: Optional callable to generate remediation commands.
"""
name: str
action_fn: Callable[[HookContext, Sequence[str]], bool]
is_mutating: bool
extensions: Sequence[str] | None = None
remediation_cmd_fn: Callable[[Sequence[str]], Sequence[str]] | None = None
def format_code_action(
context: HookContext,
files: Sequence[str],
) -> bool:
"""Executes fx format-code on target files.
Args:
context: HookContext with repo_root, check_only, is_agent, and reporter.
files: Sequence of file paths relative to repo_root.
Returns:
True if formatting succeeded or fail-opened, False otherwise.
"""
if not files:
return True
reporter = context.reporter
is_agent = context.is_agent
fx_cmd = None
try:
candidate = find_fuchsia_dir(context.repo_root) / "scripts" / "fx"
if candidate.is_file():
fx_cmd = str(candidate)
except RuntimeError:
pass
if fx_cmd is None and shutil.which("fx"):
fx_cmd = "fx"
if fx_cmd is None:
if not is_agent:
reporter.on_warning(
"`fx` not found on PATH or in scripts/fx. Skipping code formatting."
)
return True
reporter.on_error(
"`fx` command not found on PATH or in scripts/fx in agent environment."
)
return False
cmd = [fx_cmd, "format-code", f"--files={','.join(files)}"]
res = run_cmd(cmd, cwd=context.repo_root)
if res.returncode != 0:
output = "\n".join(
filter(None, [res.stdout.strip(), res.stderr.strip()])
)
if not output:
output = f"Command {cmd[0]} failed with exit code {res.returncode}"
reporter.on_error(output)
return False
if context.check_only:
diff_res = run_cmd(
["git", "diff", "--quiet", "--", *files],
cwd=context.repo_root,
)
if diff_res.returncode != 0:
return False
return True
DEFAULT_PRE_COMMIT_ACTIONS: tuple[HookAction, ...] = (
HookAction(
name="fx_format_code",
action_fn=format_code_action,
is_mutating=True,
extensions=DEFAULT_FORMATTABLE_EXTENSIONS,
remediation_cmd_fn=lambda files: [
f"fx format-code --files={','.join(files)}"
],
),
)
def commit_msg_action(
context: HookContext,
files: Sequence[str],
) -> bool:
"""Executes commit_msg_checker.py on a commit message file.
Args:
context: HookContext with repo_root, is_agent, and reporter.
files: Sequence of file paths containing the commit message file.
Returns:
True if commit message check passed, False otherwise.
"""
if not files:
return True
reporter = context.reporter
is_agent = context.is_agent
msg_file = Path(files[0])
try:
fuchsia_dir = find_fuchsia_dir(context.repo_root)
except RuntimeError:
fuchsia_dir = context.repo_root
checker_script = fuchsia_dir / "scripts" / "shac" / "commit_msg_checker.py"
if not checker_script.is_file():
if is_agent:
reporter.on_error(
f"Commit message checker script not found: {checker_script}"
)
return False
reporter.on_warning(
f"Commit message checker script not found: {checker_script}. Skipping check."
)
return True
python_bin = fuchsia_dir / "scripts" / "fuchsia-vendored-python"
py_exec = str(python_bin) if python_bin.is_file() else sys.executable
cmd = [py_exec, str(checker_script), "--message-file", str(msg_file)]
if is_agent:
cmd.append("--strict")
res = run_cmd(cmd, cwd=context.repo_root)
if res.returncode != 0:
output = "\n".join(
filter(None, [res.stdout.strip(), res.stderr.strip()])
)
if not output:
output = f"Command {cmd[0]} failed with exit code {res.returncode}"
reporter.on_error(output)
return False
# In non-strict mode (human developer), surface advisory warnings if any
output = res.stdout.strip()
if output and not is_agent:
reporter.on_warning(output)
return True