feat(FN-5089): complete Step 1 — add commit-msg hook installer

Fusion-Task-Id: FN-5089
Fusion-Task-Lineage: ad7ea32a-c024-45bb-b68e-b65665b9b7c6
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 02:05:32 -07:00
committed by gsxdsm
parent 17ace6fef0
commit 96eb610800

View File

@@ -6,6 +6,7 @@ import { promisify } from "node:util";
const execAsync = promisify(exec);
export const DEFAULT_ALLOWED_BRANCH_PATTERNS = ["^fusion/step-\\d+-[a-z0-9-]+$"] as const;
const COMMIT_MSG_HOOK_MARKER = "# fusion-managed-commit-msg-hook";
function toShellCasePattern(pattern: string): string {
return pattern
@@ -61,6 +62,42 @@ async function resolveGitPath(worktreePath: string, gitPath: string): Promise<st
}
}
export function buildCommitMsgTrailerHook(
taskId: string,
options: {
taskPrefix?: string;
trailerName?: string;
} = {}
): string {
const taskPrefix = (options.taskPrefix ?? "FN").trim() || "FN";
const trailerName = (options.trailerName ?? "Fusion-Task-Id").trim() || "Fusion-Task-Id";
return `#!/bin/sh
set -eu
${COMMIT_MSG_HOOK_MARKER}
# fusion-task-id-seed: ${taskId}
TASK_FILE=$(git rev-parse --git-path fusion-task-id)
[ -f "$TASK_FILE" ] || exit 0
TASK_ID=$(cat "$TASK_FILE")
[ -n "$TASK_ID" ] || exit 0
PREFIX=${JSON.stringify(taskPrefix)}
case "$TASK_ID" in
${taskPrefix}-*) ;;
*) TASK_ID="$PREFIX-$(printf '%s' "$TASK_ID" | sed -E "s/^${taskPrefix}-//i")" ;;
esac
TRAILER_NAME=${JSON.stringify(trailerName)}
git interpret-trailers \
--in-place \
--if-exists doNothing \
--trailer "$TRAILER_NAME: $TASK_ID" \
"$1"
`;
}
async function writeFileAtomic(targetPath: string, content: string, mode?: number): Promise<void> {
await execAsync(`mkdir -p ${JSON.stringify(dirname(targetPath))}`);
const tmpPath = `${targetPath}.tmp`;
@@ -71,10 +108,35 @@ async function writeFileAtomic(targetPath: string, content: string, mode?: numbe
await fs.rename(tmpPath, targetPath);
}
async function installCommitMsgHook(input: {
worktreePath: string;
taskId: string;
taskPrefix: string;
trailerName: string;
}): Promise<void> {
const hookPath = await resolveGitPath(input.worktreePath, "hooks/commit-msg");
const existing = await fs.readFile(hookPath, "utf-8").catch(() => null);
if (existing && !existing.includes(COMMIT_MSG_HOOK_MARKER)) {
console.warn(
`[worktree-hooks] commit-msg hook already exists at ${hookPath}; skipping Fusion trailer hook install for ${input.taskId}`
);
return;
}
const hook = buildCommitMsgTrailerHook(input.taskId, {
taskPrefix: input.taskPrefix,
trailerName: input.trailerName,
});
await writeFileAtomic(hookPath, hook, 0o755);
}
export async function installTaskWorktreeIdentityGuard(input: {
worktreePath: string;
taskId: string;
allowedBranchPatterns?: readonly string[];
commitMsgHookEnabled?: boolean;
taskPrefix?: string;
taskAttributionTrailerName?: string;
}): Promise<void> {
const hook = buildIdentityGuardHook(input.taskId, input.allowedBranchPatterns ?? DEFAULT_ALLOWED_BRANCH_PATTERNS);
const metadataPath = await resolveGitPath(input.worktreePath, "fusion-task-id");
@@ -82,4 +144,13 @@ export async function installTaskWorktreeIdentityGuard(input: {
await writeFileAtomic(metadataPath, `${input.taskId}\n`);
await writeFileAtomic(hookPath, hook, 0o755);
if (input.commitMsgHookEnabled !== false) {
await installCommitMsgHook({
worktreePath: input.worktreePath,
taskId: input.taskId,
taskPrefix: input.taskPrefix ?? "FN",
trailerName: input.taskAttributionTrailerName ?? "Fusion-Task-Id",
});
}
}