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
2 changes: 1 addition & 1 deletion apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6041,7 +6041,7 @@ it.layer(layerGitManagerTest)("GitManager", (it) => {
);

expect(errorMessage).toContain("Git command failed in GitVcsDriver.commit.commit");
expect(errorMessage).not.toContain("hook: fail");
expect(errorMessage).toContain("hook: fail");
expect(events).toEqual(
expect.arrayContaining([
expect.objectContaining({
Expand Down
38 changes: 38 additions & 0 deletions apps/server/src/vcs/GitVcsDriverCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3415,6 +3415,44 @@ it.layer(layerTest)("GitVcsDriver core integration", (it) => {
}),
);

it.effect("surfaces the output of a hook that rejected the commit", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;
const fileSystem = yield* FileSystem.FileSystem;
const pathService = yield* Path.Path;

const hookOutput = "commit message is missing Signed-off-by";
yield* writeTextFile(
cwd,
".git/hooks/pre-commit",
`#!/bin/sh\necho "${hookOutput}" >&2\nexit 1\n`,
);
yield* fileSystem.chmod(pathService.join(cwd, ".git/hooks/pre-commit"), 0o755);
yield* writeTextFile(cwd, "a.txt", "a\n");

const error = yield* driver
.commit(cwd, "Add a", "", { stage: { filePaths: ["a.txt"] } })
.pipe(Effect.flip);

assert.equal(error._tag, "GitCommandError");
assert.include(error.message, hookOutput);
}),
);

it.effect("says when there is nothing to commit", () =>
Effect.gen(function* () {
const cwd = yield* makeTmpDir();
yield* initRepoWithCommit(cwd);
const driver = yield* GitVcsDriver.GitVcsDriver;

const error = yield* driver.commit(cwd, "Empty", "").pipe(Effect.flip);

assert.include(error.message, "nothing to commit");
}),
);

it.effect.each([false, true])(
"preserves partial staging and split-index files (split: %s)",
(split) =>
Expand Down
26 changes: 24 additions & 2 deletions apps/server/src/vcs/GitVcsDriverCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,18 @@ function classifyGitFailure(stderr: string): GitCommandFailureReason | null {
return null;
}

const COMMIT_FAILURE_OUTPUT_MAX_CHARS = 1_000;

// `git commit` is local, so its output carries no remote URLs; it is surfaced
// because a rejecting hook's own words are the only explanation of why.
function commitFailureOutput({
stdout,
stderr,
}: Pick<GitVcsDriver.ExecuteGitResult, "stdout" | "stderr">): string {
const output = (stderr.trim() || stdout.trim()).slice(-COMMIT_FAILURE_OUTPUT_MAX_CHARS);
return output.length > 0 ? output : "Git command exited with a non-zero status.";
}

function parseDefaultBranchFromRemoteHeadRef(value: string, remoteName: string): string | null {
const trimmed = value.trim();
const prefix = `refs/remotes/${remoteName}/`;
Expand Down Expand Up @@ -2250,10 +2262,20 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
onStderrLine: (line: string) =>
options.progress?.onOutputLine?.({ stream: "stderr", text: line }) ?? Effect.void,
};
yield* executeGit("GitVcsDriver.commit.commit", cwd, args, {
const commitResult = yield* executeGit("GitVcsDriver.commit.commit", cwd, args, {
...(options?.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
...(progress ? { progress } : {}),
}).pipe(Effect.asVoid);
allowNonZeroExit: true,
});
if (commitResult.exitCode !== 0) {
return yield* new GitCommandError({
...gitCommandContext({ operation: "GitVcsDriver.commit.commit", cwd, args }),
detail: commitFailureOutput(commitResult),
...(commitResult.exitCode === null ? {} : { exitCode: commitResult.exitCode }),
stdoutLength: commitResult.stdout.length,
stderrLength: commitResult.stderr.length,
});
}
const commitSha = yield* runGitStdout("GitVcsDriver.commit.revParseHead", cwd, [
"rev-parse",
"HEAD",
Expand Down
Loading