Skip to content

Commit 442b4f8

Browse files
recuu-pfegclaude
andcommitted
feat: auto-review git hook — toban init installs pre-push hook
On toban init, installs .git/hooks/pre-push that runs toban review --diff in background after each push. Skips if existing hook found. Only affects user pushes, not agent pushes via CLI run-loop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dd297a5 commit 442b4f8

1 file changed

Lines changed: 70 additions & 2 deletions

File tree

src/commands/init.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,66 @@
99
*/
1010

1111
import * as p from "@clack/prompts";
12-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
1313
import { join } from "node:path";
1414
import { execSync } from "node:child_process";
1515
import { createApiClient, type WorkspaceInfo } from "../api-client.js";
1616

17+
// ---------------------------------------------------------------------------
18+
// Git hook installer
19+
// ---------------------------------------------------------------------------
20+
21+
function findGitDir(cwd: string): string | null {
22+
try {
23+
return execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" }).toString().trim();
24+
} catch {
25+
return null;
26+
}
27+
}
28+
29+
function installPostPushHook(cwd: string, apiUrl: string, apiKey: string): boolean {
30+
const gitDir = findGitDir(cwd);
31+
if (!gitDir) return false;
32+
33+
const hooksDir = join(gitDir, "hooks");
34+
mkdirSync(hooksDir, { recursive: true });
35+
36+
// post-push is not a native git hook, so we use post-commit + push detection
37+
// Instead, use pre-push which fires before push completes
38+
const hookPath = join(hooksDir, "pre-push");
39+
40+
// Don't overwrite existing hooks
41+
if (existsSync(hookPath)) {
42+
const existing = readFileSync(hookPath, "utf-8");
43+
if (existing.includes("toban review")) return true; // already installed
44+
return false; // user has a custom hook
45+
}
46+
47+
const hookScript = `#!/bin/sh
48+
# Auto-review on push — installed by toban init
49+
# Runs toban review asynchronously after push completes
50+
# Only reviews if the push succeeds (pre-push runs before push, so we background it)
51+
52+
# Get the commit range being pushed
53+
while read local_ref local_sha remote_ref remote_sha; do
54+
if [ "$local_sha" != "0000000000000000000000000000000000000000" ]; then
55+
# Background: wait for push to finish, then review
56+
(
57+
sleep 2
58+
npx toban review --api-url "${apiUrl}" --api-key "${apiKey}" --diff "$remote_sha..$local_sha" 2>/dev/null &
59+
) &
60+
fi
61+
done
62+
63+
exit 0
64+
`;
65+
66+
writeFileSync(hookPath, hookScript);
67+
chmodSync(hookPath, 0o755);
68+
return true;
69+
}
70+
71+
1772
// ---------------------------------------------------------------------------
1873
// Config types
1974
// ---------------------------------------------------------------------------
@@ -270,7 +325,20 @@ export async function handleInit(): Promise<void> {
270325

271326
p.log.success(`Config saved to ${CONFIG_DIR}/${CONFIG_FILE}`);
272327

273-
// 7. Optionally create first sprint
328+
// 7. Install git pre-push hook for auto-review
329+
const hookInstalled = installPostPushHook(cwd, apiUrl, apiKey);
330+
if (hookInstalled) {
331+
p.log.success("Git pre-push hook installed (auto-review on push)");
332+
} else {
333+
const gitDir = findGitDir(cwd);
334+
if (!gitDir) {
335+
p.log.warning("Not a git repository — skipping auto-review hook");
336+
} else {
337+
p.log.warning("Existing pre-push hook found — auto-review not installed");
338+
}
339+
}
340+
341+
// 8. Optionally create first sprint
274342
const createSprint = await p.confirm({
275343
message: "Create and start the first sprint now?",
276344
initialValue: false,

0 commit comments

Comments
 (0)