Skip to content
Draft
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
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ portions of the CI gate:
bun run setup:hooks
```

This installs a `pre-push` hook (into the hooks dir git reports, so worktrees and
`core.hooksPath` work) that runs `bun run prepush` — `typecheck`,
This installs a `pre-push` hook into this repository's hooks directory (including
linked-worktree layouts). For safety, setup refuses to replace hooks when
`core.hooksPath` redirects them to a potentially shared directory. The hook runs
`bun run prepush` — `typecheck`,
`lint:gui:if-changed`, `test`, `privacy:scan`, and `doctor:gui:if-changed` —
before every `git push`. Both `lint:gui:if-changed` and `doctor:gui:if-changed`
run their check only when the push touches `gui/`.
Expand Down
21 changes: 19 additions & 2 deletions scripts/setup-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,25 @@ import { join, resolve } from "node:path";

const repoRoot = resolve(import.meta.dirname, "..");

// Resolve the real hooks dir via git so linked worktrees (`.git` file), core.hooksPath,
// and non-default git dirs all work. Hard-coding <repo>/.git/hooks breaks those setups.
// A configured hooksPath may be shared by unrelated repositories. Installing our
// cwd-dependent hooks there would replace shared policy hooks and run another
// repository's package scripts. Keep installation scoped to this repository.
try {
execFileSync("git", ["config", "--get", "core.hooksPath"], {
cwd: repoRoot,
encoding: "utf8",
});
console.error("setup-hooks: refusing to install because core.hooksPath is configured.");
process.exit(1);
} catch (error) {
if (typeof error === "object" && error !== null && "status" in error && error.status !== 1) {
console.error("setup-hooks: could not inspect core.hooksPath.");
process.exit(1);
}
}

// Resolve Git's repository-local hooks dir so linked worktrees and non-default
// git dirs work without assuming that <repo>/.git is a directory.
let hooksDir: string;
try {
hooksDir = execFileSync(
Expand Down
42 changes: 42 additions & 0 deletions tests/setup-hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = fileURLToPath(new URL("../", import.meta.url));
const temporaryDirectories: string[] = [];

afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});

describe("setup-hooks", () => {
test("refuses to modify a configured shared hooksPath", () => {
const directory = mkdtempSync(join(tmpdir(), "opencodex-setup-hooks-"));
temporaryDirectories.push(directory);
const hooksDirectory = join(directory, "shared-hooks");
const configPath = join(directory, "gitconfig");
const existingHook = join(hooksDirectory, "pre-push");
mkdirSync(hooksDirectory);
writeFileSync(configPath, `[core]\n\thooksPath = ${hooksDirectory}\n`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Escape the generated hooks path for Git config

On Windows, hooksDirectory contains backslashes (typically C:\Users\...), and interpolating it verbatim into the config makes sequences such as \U invalid to Git's config parser. Consequently, git config --get exits with a parse error, the script emits could not inspect instead of the expected refusal message, and this new test fails the Windows suite. Write the value using git config --file ... core.hooksPath ..., or normalize/escape the path before writing it.

AGENTS.md reference: AGENTS.md:L136-L137

Useful? React with 👍 / 👎.

writeFileSync(existingHook, "#!/bin/sh\necho existing-policy\n");

const result = Bun.spawnSync(["bun", "scripts/setup-hooks.ts"], {
cwd: repoRoot,
env: {
...process.env,
GIT_CONFIG_GLOBAL: configPath,
GIT_CONFIG_NOSYSTEM: "1",
},
});

expect(result.exitCode).toBe(1);
expect(new TextDecoder().decode(result.stderr)).toContain(
"setup-hooks: refusing to install because core.hooksPath is configured",
);
expect(readFileSync(existingHook, "utf8")).toBe("#!/bin/sh\necho existing-policy\n");
});
});
Loading