Fuchsia Git Staging & Commit Hooks: Architecture & Reference Guide

1. Overview & Current Architecture

Fuchsia developer workflows span multiple Git repositories (//, //vendor/*, //integration) coordinated by Jiri. Ensuring that code committed locally meets platform formatting, documentation, and commit message standards is essential to preventing presubmit and CQ cycle failures.

The Git staging and hook subsystem runs pre-commit and commit-msg checks without clobbering unstaged edits:

  • Index Partitioning: Distinguishes fully staged files from partially staged files to prevent corrupting unstaged hunks during formatting.
  • Stash Isolation ($S^2 \to S$ diff apply): Uses the diff between the index commit ($S^2$) and stash worktree ($S$) to temporarily isolate staged edits without triggering false 3-way merge conflicts against HEAD.
  • Automatic Formatting: fx format-code formats fully staged files with automatic re-staging (git add). Partially staged files are evaluated strictly in read-only check mode under stash isolation.
  • Commit Message Style Verification: Validates subject line length ($\le 50$ chars recommended, $> 65$ warned), 72-character body line wrapping, and mandatory footers (Bug:, Test:, Change-Id:) via scripts/shac/commit_msg_checker.py.
  • Jiri Hook Delegation: Jiri (via //integration) distributes a hook dispatcher (hook-dispatcher.sh) and root hook wrappers (pre-commit, commit-msg). fx agents setup drops scripts into .git/hooks/<hook_name>.d/ without modifying the root wrappers.

2. Running Multiple Hooks & Worktree Support

2.1 Problem Statement

In Git worktrees (git worktree add), .git is a file pointing to the main repository, causing relative path lookups (.git/hooks/...) to fail. Furthermore, developers or other tools may have custom scripts they wish to run during pre-commit or commit-msg phases. Overwriting these scripts leads to broken workflows.

2.2 Directory-Based Hooks (<hook>.d/)

Jiri (via //integration) natively distributes the universal dispatcher (hook-dispatcher.sh) and root hook wrappers (pre-commit, commit-msg).

.git/hooks/pre-commit (Jiri Distributed Wrapper)
  │
  └──► .git/hooks/pre-commit.d/
         ├── 05-author-check.sh         (Upstream Googler email check)
         ├── 10-fuchsia-agent.sh        (Automated formatting & staging isolation)
         ├── 10-unstage-submodules.sh   (Submodule reset check)
         └── 50-team-tool.sh            (Modular team/user scripts)

2.3 How Hook Dispatch Works

The dispatcher is handled natively by Jiri executing directory-based multi-hooks (<hook_name>.d/*) in lexical order. Check //integration/git-hooks/hook-dispatcher.sh for the dispatcher implementation.

2.4 Install and Uninstall Behavior

  1. Installation (fx agents setup):
    • Resolves hooks directory via git rev-parse --git-path hooks (ensuring compatibility with Git worktrees and subdirectories).
    • install_git_hooks creates .git/hooks/<hook_name>.d/ and writes or updates 10-fuchsia-agent.sh.
    • It does NOT touch or overwrite .git/hooks/<hook_name>.
  2. Uninstallation / Reset (fx agents setup --reset):
    • Uses uninstall_git_hooks to delete only .git/hooks/<hook_name>.d/10-fuchsia-agent.sh.
    • Leaves the .git/hooks/<hook_name> wrappers and <hook_name>.d/ directory intact.
    • Any user or team scripts in <hook_name>.d/ continue executing without disruption.
    • You can inspect the status with get_git_hooks_status.

3. Subsystem Layers

The Git staging and hook subsystem is split into three layers:

  • Layer 1: Generic Staging Helpers (lib/git_staging/):
    • Standalone Git index partitioning, stash isolation (StashGuard), and pipeline execution.
    • Runs standard Git commands (git diff, git apply, git status) with no Fuchsia-specific dependencies.
  • Layer 2: Platform Hook Adapters (lib/githooks/):
    • Fuchsia-specific adapters bridging the staging engine with platform tools (fx format-code, commit_msg_checker.py).
    • Terminal reporters (ConsoleReporter) and the hook CLI dispatcher (runner.py).
  • Layer 3: Installer & Setup (commands/setup.py, lib/githooks/installer.py):
    • Discovers checkout repositories across // and //vendor/*.
    • Idempotently installs and uninstalls 10-fuchsia-agent.sh within .git/hooks/<hook_name>.d/.

For the complete repository and test file layout, see tools/agents/DEVELOPMENT.md#directory-layout.

3.1 Standalone Hook Execution

The 10-fuchsia-agent.sh hook fragment exports PYTHONPATH="$FUCHSIA_DIR/tools:$PYTHONPATH" and executes $FUCHSIA_DIR/scripts/fuchsia-vendored-python. This allows hooks to run during git commit without depending on Ninja/GN build artifacts or an active build directory.


4. Console Output & Terminal Formatting

4.1 Terminal Output & Fallbacks

ConsoleReporter formats messages based on terminal capabilities:

  • Uses UTF-8 symbols (🧹, ⚠️, ) on UTF-8 interactive terminals.
  • Falls back to ASCII tags ([FIXED], [WARN], [ERROR]) on non-UTF-8 terminals or plain consoles. Clean commits run silently.

4.2 Example Output

Partial-Staging Formatting Conflict

❌ Formatting issues in partially staged files. Auto-fix skipped to prevent stash conflicts.
  Offending files:
    - src/sys/pkg/lib/fuchsia-pkg/src/meta.rs
  Or fix directly:
    fx format-code --files=src/sys/pkg/lib/fuchsia-pkg/src/meta.rs

5. Data Structures & Interfaces

5.1 Typed Data Models

The subsystem uses frozen dataclasses and typed callables for pipeline state:

  • ActionContext: Generic execution context passed to staged pipelines (repo_root, check_only).
  • HookContext: Specializes ActionContext for Fuchsia hooks with is_agent flag and ConsoleReporter.
  • PipelineResult: Immutable result of pipeline actions, tracking success, restaged_files, partial_conflicts, and errors.
  • StagedPartition: Immutable partitioning of staged files into fully_staged and partially_staged frozensets.
  • HookAction: Specification of an action in the hook pipeline, defining name, mutate capability, formattable extensions filter, and failure remediation callbacks.
  • HookOperationResult & HookStatusResult: Structured summaries of multi-repo hook installation and status queries.

See tools/agents/lib/git_staging/engine.py and tools/agents/lib/githooks/adapters.py for the complete implementations.

5.2 Stash Isolation (StashGuard)

The StashGuard context manager handles temporary stash isolation:

  • Isolation (__enter__): Detects unstaged working tree modifications via git status --porcelain and pushes them to a temporary stash with git stash push --keep-index -q -m git-staging-isolation.
  • Restoration (__exit__): Captures the binary stash diff between the index commit and stash commit ($S^2 \to S$) via git diff --no-color --binary <stash>^2 <stash>, reapplies it via git apply --binary --allow-empty, and safely drops the stash commit on successful restoration.
  • Unstaged Deletion Handling: git stash push --keep-index omits unstaged deletions of newly added index files from the stash diff. StashGuard explicitly tracks and restores these deletions (_unstaged_deleted_files) to prevent phantom files or index corruption.
  • Exception Chaining: All Git failures during diff or apply chain the underlying exception (raise GitError(...) from exc) to preserve tracebacks.

See tools/agents/lib/git_staging/engine.py for the complete implementation.

5.3 Exception Handling

  • GitError(RuntimeError): Core exception raised on Git command failures, repository resolution issues, or stash isolation errors. Ensures clean error wrapping across CLI entrypoints and runners.

6. Bypasses & Agent Execution

6.1 Bypassing Hooks

Hooks can be bypassed using standard Git flags, environment variables, or fx agents setup (see tools/agents/README.md#git-hooks-integration):

  1. Native Git Per-Commit Bypass:
    • git commit -n or git commit --no-verify (standard Git flag, skips pre-commit and commit-msg natively).
  2. Environment Variable Bypass (Scripts / CI):
    • FUCHSIA_SKIP_HOOKS=1 git commit ... (immediately exits 0 from the hook runner).
  3. Configuration Options:
    • fx agents setup installs .git/hooks/<hook_name>.d/10-fuchsia-agent.sh.
    • fx agents setup --status: Displays current configuration and reports Git hook configuration across all checkout repositories.
    • --git-hooks / --no-git-hooks: CLI flags for fx agents setup controlling Git hook installation across checkout repositories (default: --git-hooks). Passing --no-git-hooks configures agent permissions and profiles without installing or modifying Git hooks.
    • fx agents setup --reset removes 10-fuchsia-agent.sh while leaving dispatcher and user scripts intact.
    • fx agents setup --reset --no-git-hooks: Resets Fuchsia-managed rules and configuration while preserving existing Git hooks intact (skips hook uninstallation).

6.2 Interactive Rebase

  • Automated Rebase Replay: Git does not run pre-commit or commit-msg hooks when automatically applying commits during a rebase.
  • Explicit Commits & Amends: When a developer stops during an interactive rebase (git rebase -i) to edit, amend (git commit --amend), or insert a commit, Git executes the hooks normally. The staging engine ensures newly amended or inserted commits are cleanly formatted and verified without false merge conflicts or unintended hook bypasses.

6.3 Setup Configuration vs. Runtime Agent Detection

Running fx agents setup configures the checkout, but hooks only apply agent-specific behavior when an agent is actually executing:

  • The presence of ~/.local/share/Fuchsia/agents/setup/state.json indicates that fx agents setup configured the developer checkout, but does not mean an automated AI agent is executing a given commit.
  • Developers routinely perform manual git commit commands in repositories configured with fx agents setup. These interactive human commits receive human-oriented UX (ConsoleReporter, advisory warnings, colored summaries).

How Agents Are Detected

Runtime AI agent execution is detected dynamically at commit time using canonical environment variables:

  • Agent Environment Detection: Query is_invoked_by_agent(), which checks the canonical agent environment variables defined in AGENT_ENV_VARS (ANTIGRAVITY_AGENT, GEMINI_CLI, ANTIGRAVITY_EDITOR_APP_ROOT).

When active runtime agent execution is detected:

  • Auto-Formatting: Formats fully staged files in-place and re-stages them automatically.
  • Strict Validation: Runs commit_msg_checker.py --strict so non-compliant messages are rejected before reaching CQ.

7. Failure Policies & Performance

7.1 Failure Policies (Fail-Open vs. Fail-Closed)

Failure ModePolicyActionRationale
Missing Tool / Python EnvironmentFail-OpenPrint warning to stderr; exit 0.Never block commits due to environment setup issues.
Stash Restoration FailureFail-ClosedPrint recovery command; exit 1.Prevent silent data loss or corrupted working trees.
Code Syntax / Formatter ErrorFail-ClosedPrint formatter error; exit 1.Catch broken code before creating commits.
Partially Staged Formatting ErrorFail-ClosedPrint remediation hint; exit 1.Avoid committing unformatted code or corrupting unstaged hunks.
Commit Message WarningsFail-Open (Human)
Fail-Closed (Agent)
Advisory warning for humans; error for agents.Warn human developers without blocking commits; block commits from automated agents.

7.2 Performance Optimizations

  • Fast-Path Filter: Skip invoking fx format-code entirely if the staged partition contains zero formattable files. Supported extensions match DEFAULT_FORMATTABLE_EXTENSIONS (.py, .md, .c, .cc, .cpp, .h, .hh, .hpp, .rs, .go, .fidl, .gn, .gni, .json, .json5, .cml, .proto, .ts).
  • Binary Handling: git diff --binary and git apply --binary preserve binary content without corruption, while staged path filtering targets source file extensions directly.
  • Commit Latency Target: P50 commit overhead $< 600\text{ms}$; P90 $< 1,200\text{ms}$.

8. Troubleshooting & Manual Testing

8.1 Partially Staged Formatting Conflicts

When a file contains both staged and unstaged hunks, mutating formatters cannot safely rewrite the file on disk without corrupting the unstaged hunks. If formatting issues are detected in partially staged files, the commit is blocked with instructions on how to fix it.

Developers have three standard remediation paths:

  1. Stage remaining changes: If the unstaged changes are ready, stage the whole file:
    git add <file> && git commit
    
  2. Discard unstaged changes: If the unstaged changes were accidental:
    git checkout -- <file> && git commit
    
  3. Format and stage directly:
    fx format-code --files=<file> && git add <file> && git commit
    

8.2 Stash Restoration Recovery

In the rare event that working tree restoration encounters an unexpected conflict or failure (e.g. external process modification during commit), StashGuard aborts and leaves the temporary stash at stash@{0}. The developer can inspect and recover the working tree using:

git stash list
git stash apply stash@{0}

8.3 Manual & Direct CLI Testing

Hooks can be tested directly without creating a Git commit:

# Run pre-commit hook checks on current index:
$FUCHSIA_DIR/scripts/fuchsia-vendored-python tools/agents/lib/githooks/runner.py pre-commit

# Test commit-msg hook with a sample message file:
$FUCHSIA_DIR/scripts/fuchsia-vendored-python tools/agents/lib/githooks/runner.py commit-msg path/to/COMMIT_EDITMSG

# Simulate AI agent invocation:
ANTIGRAVITY_AGENT=1 $FUCHSIA_DIR/scripts/fuchsia-vendored-python tools/agents/lib/githooks/runner.py pre-commit