Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3874,6 +3874,56 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
}),
);

const commitWithHooks = (hooks: Record<string, string>) =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
for (const [name, script] of Object.entries(hooks)) {
yield* writeTextFile(cwd, `.git/hooks/${name}`, `#!/bin/sh\n${script}\n`);
yield* fs.chmod(path.join(cwd, ".git/hooks", name), 0o755);
}
yield* writeTextFile(cwd, "a.txt", "a\n");
const events: string[] = [];
const result = yield* (yield* GitVcsDriver.GitVcsDriver)
.commit(cwd, "Add a", "", {
stage: { filePaths: ["a.txt"] },
progress: {
onHookStarted: (hookName) => Effect.sync(() => void events.push(`start ${hookName}`)),
onHookFinished: ({ hookName, exitCode }) =>
Effect.sync(() => void events.push(`finish ${hookName} ${exitCode}`)),
},
})
.pipe(Effect.result);
return { events, committed: Result.isSuccess(result) };
});

it.effect("reports each commit hook finishing with its exit code", () =>
Effect.gen(function* () {
// The pre-commit hook runs git, whose own children reuse child_id 0 in the shared trace.
const { events, committed } = yield* commitWithHooks({
"pre-commit": "git -c alias.noop='!false' noop\nexit 0",
"commit-msg": "exit 0",
});
assert.isTrue(committed);
assert.deepEqual(events, [
"start pre-commit",
"finish pre-commit 0",
"start commit-msg",
"finish commit-msg 0",
]);
}),
);

it.effect("reports the exit code of a commit hook that rejects the commit", () =>
Effect.gen(function* () {
const { events, committed } = yield* commitWithHooks({ "commit-msg": "exit 3" });
assert.isFalse(committed);
assert.deepEqual(events, ["start commit-msg", "finish commit-msg 3"]);
}),
);

it.effect.each([false, true])(
"preserves partial staging and split-index files (split: %s)",
(split) =>
Expand Down
22 changes: 10 additions & 12 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,10 +629,11 @@ const addCurrentSpanEvent =
}),
);

// Nested Git processes share the trace file but reuse child IDs in their own sessions.
function trace2ChildKey(record: Record<string, unknown>): string | null {
const childId = record.child_id;
if (typeof childId === "number" || typeof childId === "string") {
return String(childId);
return typeof record.sid === "string" ? `${record.sid}\n${childId}` : String(childId);
}
const hookName = record.hook_name;
return typeof hookName === "string" && hookName.trim().length > 0 ? hookName.trim() : null;
Expand Down Expand Up @@ -683,10 +684,6 @@ const createTrace2Monitor = Effect.fnUntraced(function* (
return;
}

if (traceRecord.success.child_class !== "hook") {
return;
}

const event = traceRecord.success.event;
const childKey = trace2ChildKey(traceRecord.success);
if (childKey === null) {
Expand All @@ -701,6 +698,9 @@ const createTrace2Monitor = Effect.fnUntraced(function* (
}

if (event === "child_start") {
if (traceRecord.success.child_class !== "hook") {
return;
}
const now = yield* DateTime.now;
hookStartByChildKey.set(childKey, { hookName, startedAtMs: DateTime.toEpochMillis(now) });
yield* addCurrentSpanEvent("git.hook.started", {
Expand All @@ -712,22 +712,20 @@ const createTrace2Monitor = Effect.fnUntraced(function* (
return;
}

if (event === "child_exit") {
if (event === "child_exit" && started) {
hookStartByChildKey.delete(childKey);
const code = traceRecord.success.exitCode;
const code = traceRecord.success.code;
const exitCode = typeof code === "number" && Number.isInteger(code) ? code : null;
const now = yield* DateTime.now;
const durationMs = started
? Math.max(0, DateTime.toEpochMillis(now) - started.startedAtMs)
: null;
const durationMs = Math.max(0, DateTime.toEpochMillis(now) - started.startedAtMs);
yield* addCurrentSpanEvent("git.hook.finished", {
hookName: started?.hookName ?? hookName,
hookName: started.hookName,
exitCode,
durationMs,
});
if (progress.onHookFinished) {
yield* progress.onHookFinished({
hookName: started?.hookName ?? hookName,
hookName: started.hookName,
exitCode,
durationMs,
});
Expand Down
Loading