From 479a5f6d3cdbba271c02a97699f499aa4caff1fb Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sun, 9 Aug 2026 18:56:48 +0900 Subject: [PATCH] fix: keep development hooks repository-local --- CONTRIBUTING.md | 6 ++++-- scripts/setup-hooks.ts | 21 ++++++++++++++++++-- tests/setup-hooks.test.ts | 42 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 tests/setup-hooks.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e28f00f8e..919ac2d04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/`. diff --git a/scripts/setup-hooks.ts b/scripts/setup-hooks.ts index c632004db..a2ee199be 100644 --- a/scripts/setup-hooks.ts +++ b/scripts/setup-hooks.ts @@ -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 /.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 /.git is a directory. let hooksDir: string; try { hooksDir = execFileSync( diff --git a/tests/setup-hooks.test.ts b/tests/setup-hooks.test.ts new file mode 100644 index 000000000..f77693532 --- /dev/null +++ b/tests/setup-hooks.test.ts @@ -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`); + 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"); + }); +});