mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
Section 10.1 promised pre-push as the blocking gate that replaces pull requests. The hook did not exist, so the document promised a check that was not there — worse than saying nothing, because a promise like that gets relied on. Until now process-gate ran only as the catch-up job in CI, which reports after the code is already in dev. The hook skips branch deletions and tags, and for a branch the remote has not seen it measures from the merge-base with dev rather than from the root, or every violation committed before the gate existed would make it impossible to pass. A missing script does not block a push: old checkouts and worktrees have to stay usable. gh is optional on purpose. Reading issue status needs the network, and a hook that cannot work on a train is a hook people switch off; offline it runs what it can and CI does the strict pass. The executable bit is the quiet part. Git skips a hook without +x and says nothing — the gate reports success by being absent. Measured on a real push: mode 644 produces zero lines from the gate and the push goes through, 755 stops it. The API cannot set the bit, so install-hooks restores it on every install. Issue: #121 User-Visible: no
50 lines
2.1 KiB
JavaScript
50 lines
2.1 KiB
JavaScript
#!/usr/bin/env node
|
|
import { execFileSync } from 'node:child_process';
|
|
import { chmodSync, existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HOOKS = ['commit-msg', 'pre-push'];
|
|
|
|
// Git skips a hook that is not executable, and says nothing about it. A hook that
|
|
// silently does not run is worse than no hook: the gate reports success by being
|
|
// absent. The bit cannot be set through the GitHub API either — a file pushed
|
|
// that way arrives as 100644 — so it is restored here, on every install.
|
|
function makeHooksExecutable(hooksDir) {
|
|
if (!existsSync(hooksDir)) return;
|
|
for (const name of readdirSync(hooksDir)) {
|
|
if (!HOOKS.includes(name)) continue;
|
|
const file = join(hooksDir, name);
|
|
try {
|
|
const mode = statSync(file).mode & 0o777;
|
|
if ((mode & 0o111) !== 0o111) chmodSync(file, mode | 0o111);
|
|
} catch {
|
|
// Windows reports modes it cannot change; git there runs hooks regardless.
|
|
}
|
|
}
|
|
}
|
|
|
|
const packageRoot = realpathSync(fileURLToPath(new URL('..', import.meta.url)));
|
|
|
|
try {
|
|
// A git dependency can run `prepare` from node_modules inside the consuming
|
|
// project's checkout. Never walk up and rewrite that unrelated repository.
|
|
const gitRoot = realpathSync(execFileSync(
|
|
'git', ['rev-parse', '--show-toplevel'],
|
|
{ cwd: packageRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
).trim());
|
|
if (gitRoot.toLowerCase() !== packageRoot.toLowerCase()
|
|
|| !existsSync(fileURLToPath(new URL('../.githooks/commit-msg', import.meta.url)))) {
|
|
throw new Error('not the House Plan checkout root');
|
|
}
|
|
execFileSync('git', ['config', 'core.hooksPath', '.githooks'], {
|
|
cwd: packageRoot, stdio: 'ignore',
|
|
});
|
|
makeHooksExecutable(join(packageRoot, '.githooks'));
|
|
console.log('House Plan: installed repository hooks from .githooks');
|
|
} catch {
|
|
// npm also runs prepare for source archives and dependency installs where
|
|
// there is no matching writable checkout. That is not an install failure.
|
|
console.log('House Plan: no matching Git checkout; hooks were not installed');
|
|
}
|