Skip to content

feat(ci): wire LOC-gate into pre-commit hook (staged files only) - #237

Merged
getappz merged 2 commits into
masterfrom
feat/git-hooks-loc-gate
Jul 18, 2026
Merged

feat(ci): wire LOC-gate into pre-commit hook (staged files only)#237
getappz merged 2 commits into
masterfrom
feat/git-hooks-loc-gate

Conversation

@getappz

@getappz getappz commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Cherry-picked the unmerged chore/branch-guard-git-hooks branch's git-hooks commit (.githooks/pre-commit/pre-push, src/cli/git.rs install command) onto current master — its second commit (worktree squash-merge skip) was dropped, already landed independently on master via a different implementation (run_git_in_ok in src/worktree.rs)
  • scripts/loc-gate.sh now accepts an optional file list: no args = full-repo scan (unchanged, CI-ready), with args = fast partial scan of just those files, skipping the whole-repo allowlist-ratchet check
  • .githooks/pre-commit now runs the LOC gate against staged *.rs files only, so violations are caught before commit rather than only in CI (which isn't wired up yet — see feat: asset MCP tool — attach/get/list/delete with storage, dedup, and tests #168, blocked on splitting src/mcp_server.rs)

Depends on #236 (the git ls-files/hidden-folder traversal fix) — this branch includes that commit too since it was built on top of it locally. Once #236 merges, this branch needs a quick rebase to drop the now-duplicate commit; I'll handle that before this one merges.

Test plan

  • cargo build/cargo fmt --check clean; cargo clippy -p agentflare --bin agentflare clean (full --all-targets clippy currently fails on an unrelated, pre-existing Windows-only issue in agent_launch.rs — filed as handoff: assign items + attach versioned assets instead of raw artifacts #169, not part of this diff)
  • bash -n syntax-checked both shell scripts
  • Manually verified in a scratch repo: small .rs file commits pass, an oversized .rs file is blocked, a commit touching no .rs files is unaffected
  • Verified live in this repo (core.hooksPath already .githooks here) — the hook fired and passed on this PR's own commit

Summary by CodeRabbit

  • New Features
    • Added agentflare git install-hooks to install and enable pre-commit and pre-push branch safeguards in the current repository.
    • pre-commit and pre-push now block committing or pushing directly to the repository’s default branch.
    • pre-commit runs a staged Rust code-size/LOC gate for staged *.rs changes.
  • Bug Fixes
    • Rust LOC validation now supports partial (staged-only) scanning, avoiding full allowlist validation when it isn’t needed.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 06a34709-6d89-4b4b-b79a-9313bf329111

📥 Commits

Reviewing files that changed from the base of the PR and between 201cec8 and a0075b9.

📒 Files selected for processing (5)
  • .githooks/pre-commit
  • .githooks/pre-push
  • scripts/loc-gate.sh
  • src/cli/git.rs
  • src/cli/mod.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/cli/git.rs
  • .githooks/pre-commit

📝 Walkthrough

Walkthrough

Adds agentflare git install-hooks, repository-local pre-commit and pre-push branch protection, and staged Rust LOC-gate validation using tracked-file and partial-scan modes.

Changes

Git hook enforcement

Layer / File(s) Summary
Partial and tracked Rust scanning
scripts/loc-gate.sh
Supports staged-path scans, uses Git-tracked Rust files for full scans, skips missing files, and limits allowlist checks to full-repository scans.
Default-branch commit and push guards
.githooks/pre-commit, .githooks/pre-push
Resolves the default branch and blocks direct commits or pushes to it; pre-commit also validates staged Rust files with the LOC gate.
CLI hook installation
src/cli/git.rs, src/cli/mod.rs
Adds agentflare git install-hooks, embeds and copies hook templates, sets Unix permissions, and configures core.hooksPath.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Agentflare
  participant GitRepository
  participant Hooks
  participant LocGate
  User->>Agentflare: run git install-hooks
  Agentflare->>GitRepository: copy pre-commit and pre-push
  Agentflare->>GitRepository: set core.hooksPath
  GitRepository->>Hooks: run hook on commit
  Hooks->>LocGate: validate staged Rust paths
  LocGate-->>Hooks: return validation status
  Hooks-->>GitRepository: allow or reject operation
Loading

Possibly related PRs

  • getappz/agentflare#218: Adds the LOC gate that this change extends with staged and partial scanning.
  • getappz/agentflare#236: Updates LOC-gate file discovery and tracked Rust-path filtering, which overlap with this change.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately highlights the main change: LOC-gate enforcement in the pre-commit hook for staged files.
Description check ✅ Passed The PR mostly matches the template with Summary and Test plan filled in, though the reviewer notes and backward-compatibility details are light.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/git-hooks-loc-gate

Comment @coderabbitai help to get the list of available commands.

getappz added 2 commits July 18, 2026 09:57
scripts/loc-gate.sh now accepts an optional file list; with no args it
does the existing full-repo git ls-files scan (CI), with args it checks
only those (pre-commit). .githooks/pre-commit calls it against staged
.rs files so violations get caught before commit, not just in CI.
@getappz
getappz force-pushed the feat/git-hooks-loc-gate branch from 201cec8 to a0075b9 Compare July 18, 2026 04:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.githooks/pre-commit:
- Around line 76-79: Update the staged Rust filename collection in the
pre-commit hook to request NUL-delimited output from git and consume it with
NUL-aware reading, preserving complete filenames—including embedded
newlines—when populating staged_rs for the LOC gate.

In @.githooks/pre-push:
- Around line 43-45: Update the branch-matching case in the pre-push hook’s read
loop to inspect remote_ref rather than local_ref, so protection applies to
pushes targeting the remote default branch while allowing pushes sourced from it
to other branches.

In `@scripts/loc-gate.sh`:
- Around line 47-54: Update the file-reading logic in the loc-gate scan loop to
count lines from each file’s staged Git index blob rather than the working-tree
path. Preserve the existing hidden-folder filtering and regular-file checks, and
ensure the pre-commit invocation continues validating the exact staged content
passed by the hook.

In `@src/cli/git.rs`:
- Around line 73-94: Update the repository-root resolution in the install-hooks
flow to obtain repo_root from git rev-parse --show-toplevel, rather than
current_dir. Preserve the existing not-a-repository error handling and use the
resolved root for .githooks creation and subsequent operations.
- Around line 34-38: Update the install-hooks flow using InstallHooksArgs and
its opts value to honor the confirmation contract: before overwriting existing
.githooks/pre-commit or .githooks/pre-push files, prompt for confirmation unless
opts.yes is true. Preserve non-interactive behavior when --yes is supplied, and
abort without overwriting if confirmation is declined.
- Around line 72-98: Update install_hooks to return a Result and propagate
failures from ensure_shared_templates, directory creation, hook copying,
permission updates, and git config operations instead of returning success or
ignoring errors. Check the git config result before printing the configured
core.hooksPath, and emit the success message only after every installation step
completes; update the related call sites and ranges around install_hooks
accordingly.
- Around line 52-62: Update ensure_shared_templates to overwrite the existing
pre-commit and pre-push files on every installation using the current PRE_COMMIT
and PRE_PUSH embedded contents; remove the existence checks while preserving
directory creation and error propagation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 79d4d6f9-d188-4318-a542-49b738c7f202

📥 Commits

Reviewing files that changed from the base of the PR and between aa4ae5d and 201cec8.

📒 Files selected for processing (5)
  • .githooks/pre-commit
  • .githooks/pre-push
  • scripts/loc-gate.sh
  • src/cli/git.rs
  • src/cli/mod.rs

Comment thread .githooks/pre-commit
Comment on lines +76 to +79
staged_rs=()
while IFS= read -r f; do
[ -n "$f" ] && staged_rs+=("$f")
done < <(git diff --cached --name-only --diff-filter=ACMR -- '*.rs' 2>/dev/null || true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use NUL-delimited staged filenames.

--name-only plus line-based read splits valid filenames containing newlines, causing the resulting fragments to be silently skipped by the LOC gate.

Proposed fix
     staged_rs=()
-    while IFS= read -r f; do
+    while IFS= read -r -d '' f; do
         [ -n "$f" ] && staged_rs+=("$f")
-    done < <(git diff --cached --name-only --diff-filter=ACMR -- '*.rs' 2>/dev/null || true)
+    done < <(git diff --cached --name-only -z --diff-filter=ACMR -- '*.rs' 2>/dev/null || true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
staged_rs=()
while IFS= read -r f; do
[ -n "$f" ] && staged_rs+=("$f")
done < <(git diff --cached --name-only --diff-filter=ACMR -- '*.rs' 2>/dev/null || true)
staged_rs=()
while IFS= read -r -d '' f; do
[ -n "$f" ] && staged_rs+=("$f")
done < <(git diff --cached --name-only -z --diff-filter=ACMR -- '*.rs' 2>/dev/null || true)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.githooks/pre-commit around lines 76 - 79, Update the staged Rust filename
collection in the pre-commit hook to request NUL-delimited output from git and
consume it with NUL-aware reading, preserving complete filenames—including
embedded newlines—when populating staged_rs for the LOC gate.

Comment thread .githooks/pre-push
Comment on lines +43 to +45
while read -r local_ref local_sha remote_ref remote_sha; do
case "$local_ref" in
refs/heads/"$default")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard the destination ref, not the source ref.

The protected branch is remote_ref. Checking local_ref allows git push origin feature:main to update the remote default branch while rejecting harmless pushes such as main:backup.

Proposed fix
 while read -r local_ref local_sha remote_ref remote_sha; do
-    case "$local_ref" in
+    case "$remote_ref" in
         refs/heads/"$default")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while read -r local_ref local_sha remote_ref remote_sha; do
case "$local_ref" in
refs/heads/"$default")
while read -r local_ref local_sha remote_ref remote_sha; do
case "$remote_ref" in
refs/heads/"$default")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.githooks/pre-push around lines 43 - 45, Update the branch-matching case in
the pre-push hook’s read loop to inspect remote_ref rather than local_ref, so
protection applies to pushes targeting the remote default branch while allowing
pushes sourced from it to other branches.

Comment thread scripts/loc-gate.sh
Comment on lines 47 to 54
while IFS= read -r -d '' file; do
# Skip anything under a hidden folder (.worktrees, .claude, .github, …) —
# never project source, regardless of tracking state.
case "$file" in
.*/*|*/.*) continue ;;
esac
[[ -f "$file" ]] || continue
lines=$(wc -l <"$file" | tr -d ' ')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate the staged blob, not the working-tree file.

The pre-commit hook passes staged paths, but wc reads their current working-tree contents. A user can stage an oversized file, replace it with a shorter working copy, and commit the oversized index version successfully. Read each partial-scan file from the Git index, or add an explicit cached-input mode used by .githooks/pre-commit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/loc-gate.sh` around lines 47 - 54, Update the file-reading logic in
the loc-gate scan loop to count lines from each file’s staged Git index blob
rather than the working-tree path. Preserve the existing hidden-folder filtering
and regular-file checks, and ensure the pre-commit invocation continues
validating the exact staged content passed by the hook.

Comment thread src/cli/git.rs
Comment on lines +34 to +38
#[derive(Args)]
pub struct InstallHooksArgs {
/// Skip the confirmation prompt (for non-interactive/scripted use).
#[arg(long)]
pub yes: bool,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Honor the confirmation contract before overwriting hooks.

--yes is documented as skipping confirmation, but opts is discarded and existing .githooks/pre-commit and .githooks/pre-push files are overwritten without consent. Prompt unless opts.yes, especially when either destination already exists.

Also applies to: 100-104, 131-137

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/git.rs` around lines 34 - 38, Update the install-hooks flow using
InstallHooksArgs and its opts value to honor the confirmation contract: before
overwriting existing .githooks/pre-commit or .githooks/pre-push files, prompt
for confirmation unless opts.yes is true. Preserve non-interactive behavior when
--yes is supplied, and abort without overwriting if confirmation is declined.

Comment thread src/cli/git.rs
Comment on lines +52 to +62
fn ensure_shared_templates() -> std::io::Result<()> {
let dir = shared_hooks_dir();
fs::create_dir_all(&dir)?;
let pc = dir.join("pre-commit");
if !pc.exists() {
fs::write(&pc, PRE_COMMIT)?;
}
let pp = dir.join("pre-push");
if !pp.exists() {
fs::write(&pp, PRE_PUSH)?;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Refresh shared templates on every installation.

Files are written only when absent, so an older Agentflare installation permanently retains stale embedded hooks. Subsequent runs copy those stale scripts instead of the current PRE_COMMIT and PRE_PUSH versions.

Proposed fix
     let pc = dir.join("pre-commit");
-    if !pc.exists() {
-        fs::write(&pc, PRE_COMMIT)?;
-    }
+    fs::write(&pc, PRE_COMMIT)?;
     let pp = dir.join("pre-push");
-    if !pp.exists() {
-        fs::write(&pp, PRE_PUSH)?;
-    }
+    fs::write(&pp, PRE_PUSH)?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn ensure_shared_templates() -> std::io::Result<()> {
let dir = shared_hooks_dir();
fs::create_dir_all(&dir)?;
let pc = dir.join("pre-commit");
if !pc.exists() {
fs::write(&pc, PRE_COMMIT)?;
}
let pp = dir.join("pre-push");
if !pp.exists() {
fs::write(&pp, PRE_PUSH)?;
}
fn ensure_shared_templates() -> std::io::Result<()> {
let dir = shared_hooks_dir();
fs::create_dir_all(&dir)?;
let pc = dir.join("pre-commit");
fs::write(&pc, PRE_COMMIT)?;
let pp = dir.join("pre-push");
fs::write(&pp, PRE_PUSH)?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/git.rs` around lines 52 - 62, Update ensure_shared_templates to
overwrite the existing pre-commit and pre-push files on every installation using
the current PRE_COMMIT and PRE_PUSH embedded contents; remove the existence
checks while preserving directory creation and error propagation.

Comment thread src/cli/git.rs
Comment on lines +72 to +98
fn install_hooks(opts: InstallHooksArgs) {
let repo_root = match std::env::current_dir() {
Ok(d) => d,
Err(e) => {
eprintln!("agentflare git install-hooks: cannot resolve cwd: {e}");
return;
}
};

// Sanity: must be inside a git repo.
if !repo_root.join(".git").exists()
&& run_git(&repo_root, &["rev-parse", "--git-dir"]).is_none()
{
eprintln!("agentflare git install-hooks: not a git repository (run inside a repo root)");
return;
}

if let Err(e) = ensure_shared_templates() {
eprintln!("agentflare git install-hooks: cannot write shared templates: {e}");
return;
}

let local_dir = repo_root.join(".githooks");
if let Err(e) = fs::create_dir_all(&local_dir) {
eprintln!("agentflare git install-hooks: cannot create {local_dir:?}: {e}");
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Propagate installation failures before reporting success.

Template, directory, copy, permission, and git config failures either return a successful CLI status or are ignored entirely. In particular, the command prints core.hooksPath as configured even when git config fails. Return a Result, check every operation, and print success only after all steps complete.

Also applies to: 111-129, 153-157

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/git.rs` around lines 72 - 98, Update install_hooks to return a Result
and propagate failures from ensure_shared_templates, directory creation, hook
copying, permission updates, and git config operations instead of returning
success or ignoring errors. Check the git config result before printing the
configured core.hooksPath, and emit the success message only after every
installation step completes; update the related call sites and ranges around
install_hooks accordingly.

Comment thread src/cli/git.rs
Comment on lines +73 to +94
let repo_root = match std::env::current_dir() {
Ok(d) => d,
Err(e) => {
eprintln!("agentflare git install-hooks: cannot resolve cwd: {e}");
return;
}
};

// Sanity: must be inside a git repo.
if !repo_root.join(".git").exists()
&& run_git(&repo_root, &["rev-parse", "--git-dir"]).is_none()
{
eprintln!("agentflare git install-hooks: not a git repository (run inside a repo root)");
return;
}

if let Err(e) = ensure_shared_templates() {
eprintln!("agentflare git install-hooks: cannot write shared templates: {e}");
return;
}

let local_dir = repo_root.join(".githooks");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Resolve the actual repository root.

When invoked from a repository subdirectory, rev-parse --git-dir passes but .githooks is created beneath that subdirectory. The relative core.hooksPath is resolved for the repository, so the installed hooks are not found. Use git rev-parse --show-toplevel as repo_root.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/git.rs` around lines 73 - 94, Update the repository-root resolution
in the install-hooks flow to obtain repo_root from git rev-parse
--show-toplevel, rather than current_dir. Preserve the existing not-a-repository
error handling and use the resolved root for .githooks creation and subsequent
operations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant