Skip to content

Commit 0438aa5

Browse files
fix(hooks): include configured hooks in scoped reads
what changed: Scoped core.hooksPath reads now request Git include expansion. A disposable global-config fixture covers ordinary includes and gitdir conditional includes through preview, install, recorded hand-on and uninstall restoration. why: Git defaults scoped config reads to excludes, so a configured hooks folder was reported as unset and lost from the hand-on record. Request the effective included value with the existing scoped reader. No alternative design was set aside. what it touched: machine.mjs, doctor tests, architecture map, changelog and research-kit README inventory. Writes still use the existing Git config writer. what you verified: verified - red-first 66 passes and one expected include-read failure; then 90 doctor/machine tests passed with zero failures. Handoff OK and preflight PASS in this checkout. The required full offline suite runs in the commit gate before acceptance. Git's config documentation confirms scoped reads need --includes. what you got wrong and fixed: nothing to report.
1 parent 3fa03fa commit 0438aa5

5 files changed

Lines changed: 39 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Each release of Research-Kit, newest first. The version is `KIT_VERSION` in
44
`research-kit/lib/core.mjs`; a release is tagged `v<version>` (ADR-0116).
55

66
## Unreleased
7+
8+
- Scoped hooksPath reads expand Git include and includeIf directives, preserving the previous hooks folder through installation and removal (#369).
79
- Fixed `AGENTS.md` corpus description to use evergreen relative references rather than stale hardcoded entry counts (#299).
810

911
- The suite's "every CLI refuses a flag it does not know" table now covers `measure`,

‎docs/ARCHITECTURE.md‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎research-kit/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ node research-kit/bin/selftest.mjs # all of it
563563
node research-kit/bin/selftest.mjs gate hook # just these files
564564
```
565565

566-
1789 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
566+
1790 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
567567
the assertions settled (ADR-0021), and each test is raced against a watchdog
568568
(`RESEARCH_KIT_TEST_TIMEOUT`, default 60s).
569569

‎research-kit/lib/machine.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ export function gitVersion(opts = {}) {
357357
/** Scope-aware readers: which hooksPath is set where. */
358358
export function hooksPath(scope = 'global', opts = {}) {
359359
const flag = { global: '--global', local: '--local', system: '--system' }[scope] ?? '--global';
360-
return git(['config', flag, '--get', 'core.hooksPath'], opts);
360+
return git(['config', flag, '--includes', '--get', 'core.hooksPath'], opts);
361361
}
362362

363363
export function hooksPathEffective(opts = {}) {

‎research-kit/test/doctor.test.mjs‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { TESTED_CLI_VERSION } from '../lib/firecrawl.mjs';
77
import { PATHS, resolve, readText, writeText, readJson } from '../lib/core.mjs';
88
import { runDoctor, machineHealth, gateHealth, editGateState, scanForSecrets } from '../lib/doctor.mjs';
99
import { installEditGate, removeEditGate, installCommitGate, removeCommitGate, settingsState, retiredRepairNote, MATCHER } from '../lib/installer.mjs';
10-
import { EDIT_GATE_HOOK, RETIRED_EDIT_GATE_HOOKS } from '../lib/machine.mjs';
10+
import { EDIT_GATE_HOOK, RETIRED_EDIT_GATE_HOOKS, hooksPath, previousHooksPath } from '../lib/machine.mjs';
1111

1212
describe('doctor');
1313

@@ -26,6 +26,39 @@ function machine({ config = {}, settings = undefined } = {}) {
2626

2727
const find = (findings, name) => findings.find((f) => f.name === name);
2828

29+
test('included global hooks are recorded, preserved and restored by the commit-gate install', () => {
30+
requireGit('global include-based hooks');
31+
for (const conditional of [false, true]) {
32+
const { dir, env } = machine();
33+
const repo = path.join(dir, 'repo');
34+
fs.mkdirSync(repo);
35+
const globalFile = path.join(dir, 'gitconfig');
36+
const gitPaths = { cwd: repo, env: { ...env, GIT_CONFIG_GLOBAL: globalFile, GIT_CONFIG_NOSYSTEM: '1' } };
37+
const initialized = spawnSync('git', fixtureInitArgs(repo), { env: gitPaths.env, encoding: 'utf8', windowsHide: true });
38+
assert.equal(initialized.status, 0, initialized.stderr);
39+
const old = path.join(dir, 'included-hooks');
40+
writeText(path.join(old, 'pre-commit'), '#!/bin/sh\n# somebody else\n');
41+
const included = path.join(dir, 'included.gitconfig');
42+
writeText(included, `[core]\n hooksPath = ${JSON.stringify(old.replace(/\\/g, '/'))}\n`);
43+
const heading = conditional
44+
? `[includeIf "gitdir:${repo.replace(/\\/g, '/')}/"]`
45+
: '[include]';
46+
writeText(globalFile, `${heading}\n path = ${JSON.stringify(included.replace(/\\/g, '/'))}\n`);
47+
assert.equal(hooksPath('global', gitPaths), old.replace(/\\/g, '/'));
48+
const preview = installCommitGate({ kitHome: KIT_ROOT, env, gitPaths, cwd: repo, dryRun: true });
49+
assert.equal(preview.previous, old.replace(/\\/g, '/'));
50+
const installed = installCommitGate({ kitHome: KIT_ROOT, env, gitPaths, cwd: repo });
51+
assert.equal(installed.ok, true);
52+
assert.equal(installed.previous, old.replace(/\\/g, '/'));
53+
assert.equal(previousHooksPath(gitPaths), old.replace(/\\/g, '/'));
54+
const state = readJson(env.RESEARCH_KIT_INSTALL_STATE);
55+
assert.equal(state.previousHooksPath, old.replace(/\\/g, '/'));
56+
const removed = removeCommitGate({ env, gitPaths });
57+
assert.equal(removed.restored, old.replace(/\\/g, '/'));
58+
assert.equal(hooksPath('global', gitPaths), old.replace(/\\/g, '/'));
59+
}
60+
});
61+
2962
// A no-hit result describes inspected files. Reaching the count at the final file is
3063
// different from returning at that count while another path remains uninspected.
3164
test('secret cap: coverage distinguishes a complete boundary from an uninspected path', () => {

0 commit comments

Comments
 (0)