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: 2 additions & 0 deletions cli/src/runtime/git/git-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ export class GitAdapter implements VersionControl {
if (!content.includes(line)) return false;

const kept = content.split("\n").filter((entry) => entry.trim() !== line);
const executable = await this.fs.isExecutable(hookPath);
await this.fs.writeFile(hookPath, kept.join("\n"));
if (executable) await this.fs.chmodExecutable(hookPath);
return true;
}

Expand Down
83 changes: 83 additions & 0 deletions cli/tests/runtime/git/git-adapter.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { spawnSync } from "node:child_process";
import { chmod, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { sessionTrailerHookLine } from "../../../src/contexts/telemetry/domain/formats/commit-session-trailer.js";
import { FileAdapter } from "../../../src/runtime/filesystem/file-adapter.js";
import { HasherAdapter } from "../../../src/runtime/filesystem/hasher-adapter.js";
import { GitAdapter } from "../../../src/runtime/git/git-adapter.js";
import { environmentWithoutGitVariables } from "../../../src/runtime/git/git-environment.js";

const DELEGATE = "aidd-session-trailer.sh";
const SCRIPT = "#!/bin/sh\necho delegate\n";

function git(cwd: string, ...args: string[]): string {
const result = spawnSync(
"git",
["-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false", ...args],
{ cwd, encoding: "utf8", env: environmentWithoutGitVariables() }
);
if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`);
return result.stdout.trim();
}

describe("GitAdapter", () => {
let root: string;
let hooksDir: string;
let adapter: GitAdapter;

beforeEach(async () => {
root = await realpath(await mkdtemp(join(tmpdir(), "aidd-git-adapter-")));
git(root, "init", "-q");
hooksDir = join(root, ".git", "hooks");
adapter = new GitAdapter(new FileAdapter(new HasherAdapter()));
});

afterEach(async () => {
await rm(root, { recursive: true, force: true });
});

describe("removeCommitMessageDelegate", () => {
it("drops the one line and deletes the delegate, leaving the rest of the hook", async () => {
await mkdir(hooksDir, { recursive: true });
await writeFile(join(hooksDir, "prepare-commit-msg"), "#!/bin/sh\necho mine\n");
await adapter.installCommitMessageDelegate(root, DELEGATE, SCRIPT);

const result = await adapter.removeCommitMessageDelegate(root, DELEGATE);

expect(result).toStrictEqual({ removed: true });
expect(await readFile(join(hooksDir, "prepare-commit-msg"), "utf8")).toBe(
"#!/bin/sh\necho mine\n"
);
expect(await adapter.readCommitTrailerSetup(root, DELEGATE, "X", 1)).toStrictEqual({
delegate: "absent",
hookExecutable: true,
callSite: "missing",
hookHasOtherContent: true,
hooksDir,
});
});

// A mode bit is POSIX: on Windows `access(X_OK)` answers like `F_OK`.
it.skipIf(process.platform === "win32")(
"leaves a hook that was not executable as it found it",
async () => {
await mkdir(hooksDir, { recursive: true });
await adapter.installCommitMessageDelegate(root, DELEGATE, SCRIPT);
const hookPath = join(hooksDir, "prepare-commit-msg");
await writeFile(
hookPath,
`#!/bin/sh\n${sessionTrailerHookLine(join(hooksDir, DELEGATE))}\n`
);
await chmod(hookPath, 0o644);

await adapter.removeCommitMessageDelegate(root, DELEGATE);

expect((await adapter.readCommitTrailerSetup(root, DELEGATE, "X", 1)).hookExecutable).toBe(
false
);
}
);
});
});