huskies: merge 980
This commit is contained in:
@@ -75,6 +75,12 @@ The frontend is embedded into the Rust binary via `rust-embed`. Run `npm run bui
|
||||
|
||||
Clippy is zero-tolerance: no warnings allowed. Fix every warning before committing.
|
||||
|
||||
## Pre-commit hook
|
||||
|
||||
Every agent worktree has a pre-commit hook installed at `.git-hooks/pre-commit` that runs `script/check` (fmt-check, clippy, cargo check, source-map-check) before every `git commit`. If the hook fails, fix the issues shown and re-run `script/check` to validate.
|
||||
|
||||
`git commit --no-verify` bypasses the hook. Do **not** use it. The hook exists to prevent broken commits from reaching the merge gate; bypassing it defeats the purpose and wastes CI cycles.
|
||||
|
||||
## File size
|
||||
Target a maximum of 800 lines per source file as a soft guide. If a file grows beyond 800 lines, decompose it by concern into smaller modules. Split at natural seams: group related types, functions, or handlers together and move each cohesive group to its own file. This keeps files readable and diffs focused.
|
||||
|
||||
|
||||
+15
-6
@@ -1,8 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fast compile-only check + doc-coverage check on changed files.
|
||||
# Use this for rapid iteration feedback while writing code.
|
||||
# Catches the doc-coverage gate failures locally instead of waiting for
|
||||
# the merge gate to bounce on a single missing `///`.
|
||||
# Pre-commit quality gate: fmt-check, clippy, cargo check, and doc-coverage.
|
||||
# Run this before committing to catch fmt drift, clippy warnings, compile
|
||||
# errors, and missing doc comments without waiting for the full test suite.
|
||||
set -euo pipefail
|
||||
cargo check --tests --workspace
|
||||
cargo run -p source-map-gen --bin source-map-check -- --worktree . --base master
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
echo "=== Checking Rust formatting ==="
|
||||
cargo fmt --manifest-path "$PROJECT_ROOT/Cargo.toml" --all --check
|
||||
|
||||
echo "=== Running cargo clippy ==="
|
||||
cargo clippy --manifest-path "$PROJECT_ROOT/Cargo.toml" --workspace --all-targets -- -D warnings
|
||||
|
||||
echo "=== Checking doc coverage on changed files ==="
|
||||
cargo run --manifest-path "$PROJECT_ROOT/Cargo.toml" -p source-map-gen --bin source-map-check --quiet -- --worktree "$PROJECT_ROOT" --base master
|
||||
|
||||
@@ -195,6 +195,13 @@ pub(super) async fn run_agent_spawn(
|
||||
}
|
||||
};
|
||||
|
||||
// Step 1.1: Install the pre-commit quality-gate hook in the worktree.
|
||||
// Non-fatal — if installation fails the agent can still run; the failure
|
||||
// is logged so the operator can investigate.
|
||||
if let Err(e) = crate::worktree::install_pre_commit_hook(&wt_info.path) {
|
||||
slog_error!("[agents] pre-commit hook install failed for {sid}: {e}");
|
||||
}
|
||||
|
||||
// Step 1.5: Update the source map for changed files since master.
|
||||
// Non-blocking — failures are logged but do not gate the spawn.
|
||||
{
|
||||
|
||||
@@ -89,6 +89,69 @@ pub(crate) async fn run_teardown_commands(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Install a pre-commit git hook in an agent worktree.
|
||||
///
|
||||
/// Creates `{wt_path}/.git-hooks/pre-commit` containing a shell script that
|
||||
/// runs `script/check` and aborts the commit if it fails. Configures the
|
||||
/// worktree's `core.hooksPath` (via `git config --worktree`) so only this
|
||||
/// worktree uses the per-worktree hooks directory.
|
||||
pub fn install_pre_commit_hook(wt_path: &Path) -> Result<(), String> {
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let hooks_dir = wt_path.join(".git-hooks");
|
||||
std::fs::create_dir_all(&hooks_dir).map_err(|e| format!("create .git-hooks dir: {e}"))?;
|
||||
|
||||
let hook = "#!/bin/sh\n\
|
||||
#\n\
|
||||
# Pre-commit hook installed by huskies.\n\
|
||||
# Runs script/check (fmt-check, clippy, cargo check, source-map-check)\n\
|
||||
# before every commit. Aborts if any gate fails.\n\
|
||||
#\n\
|
||||
# Emergency bypass: git commit --no-verify (see AGENT.md — avoid this)\n\
|
||||
\n\
|
||||
REPO_ROOT=\"$(git rev-parse --show-toplevel)\"\n\
|
||||
\n\
|
||||
printf '[pre-commit] Running script/check ...\\n'\n\
|
||||
OUTPUT=$(\"$REPO_ROOT/script/check\" 2>&1)\n\
|
||||
STATUS=$?\n\
|
||||
\n\
|
||||
if [ \"$STATUS\" -ne 0 ]; then\n\
|
||||
printf '\\n=== PRE-COMMIT HOOK FAILED ===\\n\\n'\n\
|
||||
printf '%s\\n' \"$OUTPUT\"\n\
|
||||
printf '\\nFix the issues above, then re-validate with:\\n'\n\
|
||||
printf ' script/check\\n'\n\
|
||||
printf '\\nEmergency bypass (see AGENT.md -- avoid this):\\n'\n\
|
||||
printf ' git commit --no-verify\\n\\n'\n\
|
||||
exit 1\n\
|
||||
fi\n";
|
||||
|
||||
let hook_path = hooks_dir.join("pre-commit");
|
||||
std::fs::write(&hook_path, hook).map_err(|e| format!("write pre-commit hook: {e}"))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
std::fs::set_permissions(&hook_path, std::fs::Permissions::from_mode(0o755))
|
||||
.map_err(|e| format!("chmod pre-commit hook: {e}"))?;
|
||||
|
||||
// Point git at the per-worktree hooks dir so only this worktree uses
|
||||
// these hooks (not the main repo or other worktrees).
|
||||
// Requires extensions.worktreeConfig = true in the repository config.
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["config", "--worktree", "core.hooksPath", ".git-hooks"])
|
||||
.current_dir(wt_path)
|
||||
.output()
|
||||
.map_err(|e| format!("git config --worktree core.hooksPath: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!(
|
||||
"git config --worktree core.hooksPath failed: {stderr}"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn run_shell_command(cmd: &str, cwd: &Path) -> Result<(), String> {
|
||||
let cmd = cmd.to_string();
|
||||
let cwd = cwd.to_path_buf();
|
||||
@@ -335,4 +398,79 @@ mod tests {
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_pre_commit_hook_creates_executable_hook_and_sets_hookspath() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let project_root = tmp.path().join("main-repo");
|
||||
fs::create_dir_all(&project_root).unwrap();
|
||||
init_git_repo(&project_root);
|
||||
|
||||
// Enable per-worktree config so git config --worktree works.
|
||||
Command::new("git")
|
||||
.args(["config", "extensions.worktreeConfig", "true"])
|
||||
.current_dir(&project_root)
|
||||
.output()
|
||||
.expect("enable extensions.worktreeConfig");
|
||||
|
||||
// Create a linked worktree to simulate what huskies does for agents.
|
||||
let wt_path = tmp.path().join("linked-wt");
|
||||
let out = Command::new("git")
|
||||
.args([
|
||||
"worktree",
|
||||
"add",
|
||||
wt_path.to_str().unwrap(),
|
||||
"-b",
|
||||
"feature/hook-test",
|
||||
])
|
||||
.current_dir(&project_root)
|
||||
.output()
|
||||
.expect("git worktree add");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"git worktree add failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
|
||||
install_pre_commit_hook(&wt_path).expect("install_pre_commit_hook must succeed");
|
||||
|
||||
// Hook file must exist.
|
||||
let hook_path = wt_path.join(".git-hooks").join("pre-commit");
|
||||
assert!(hook_path.exists(), "pre-commit hook must be created");
|
||||
|
||||
// Hook must be executable.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = fs::metadata(&hook_path).unwrap().permissions().mode();
|
||||
assert!(mode & 0o111 != 0, "pre-commit hook must be executable");
|
||||
}
|
||||
|
||||
// Hook content must reference script/check and --no-verify.
|
||||
let content = fs::read_to_string(&hook_path).unwrap();
|
||||
assert!(
|
||||
content.contains("script/check"),
|
||||
"hook must invoke script/check; got:\n{content}"
|
||||
);
|
||||
assert!(
|
||||
content.contains("--no-verify"),
|
||||
"hook must mention --no-verify bypass; got:\n{content}"
|
||||
);
|
||||
|
||||
// git config core.hooksPath for the worktree must be .git-hooks.
|
||||
let cfg_out = Command::new("git")
|
||||
.args(["config", "--worktree", "core.hooksPath"])
|
||||
.current_dir(&wt_path)
|
||||
.output()
|
||||
.expect("git config --worktree core.hooksPath");
|
||||
assert!(
|
||||
cfg_out.status.success(),
|
||||
"git config --worktree core.hooksPath lookup failed"
|
||||
);
|
||||
let value = String::from_utf8_lossy(&cfg_out.stdout).trim().to_string();
|
||||
assert_eq!(
|
||||
value, ".git-hooks",
|
||||
"core.hooksPath must be set to .git-hooks"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ mod sweep;
|
||||
|
||||
pub use cleanup::{format_report, run_cleanup};
|
||||
pub use create::create_worktree;
|
||||
pub use create::install_pre_commit_hook;
|
||||
pub use git::migrate_slug_paths;
|
||||
pub use remove::remove_worktree_by_story_id;
|
||||
pub use sweep::sweep_orphaned_worktrees;
|
||||
|
||||
Reference in New Issue
Block a user