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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Each release of Research-Kit, newest first. The version is `KIT_VERSION` in
`research-kit/lib/core.mjs`; a release is tagged `v<version>` (ADR-0116).

## Unreleased

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

- The suite's "every CLI refuses a flag it does not know" table now covers `measure`,
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion research-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ node research-kit/bin/selftest.mjs # all of it
node research-kit/bin/selftest.mjs gate hook # just these files
```

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

Expand Down
2 changes: 1 addition & 1 deletion research-kit/lib/machine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ export function gitVersion(opts = {}) {
/** Scope-aware readers: which hooksPath is set where. */
export function hooksPath(scope = 'global', opts = {}) {
const flag = { global: '--global', local: '--local', system: '--system' }[scope] ?? '--global';
return git(['config', flag, '--get', 'core.hooksPath'], opts);
return git(['config', flag, '--includes', '--get', 'core.hooksPath'], opts);
}

export function hooksPathEffective(opts = {}) {
Expand Down
39 changes: 38 additions & 1 deletion research-kit/test/doctor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { TESTED_CLI_VERSION } from '../lib/firecrawl.mjs';
import { PATHS, resolve, readText, writeText, readJson } from '../lib/core.mjs';
import { runDoctor, machineHealth, gateHealth, editGateState, scanForSecrets } from '../lib/doctor.mjs';
import { installEditGate, removeEditGate, installCommitGate, removeCommitGate, settingsState, retiredRepairNote, MATCHER } from '../lib/installer.mjs';
import { EDIT_GATE_HOOK, RETIRED_EDIT_GATE_HOOKS } from '../lib/machine.mjs';
import { EDIT_GATE_HOOK, RETIRED_EDIT_GATE_HOOKS, hooksPath, previousHooksPath } from '../lib/machine.mjs';

describe('doctor');

Expand All @@ -26,6 +26,43 @@ function machine({ config = {}, settings = undefined } = {}) {

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

test('included global hooks are recorded, preserved and restored by the commit-gate install', () => {
requireGit('global include-based hooks');
for (const conditional of [false, true]) {
const { dir, env } = machine();
const repo = path.join(dir, 'repo');
fs.mkdirSync(repo);
const globalFile = path.join(dir, 'gitconfig');
const gitPaths = { cwd: repo, env: { ...env, GIT_CONFIG_GLOBAL: globalFile, GIT_CONFIG_NOSYSTEM: '1' } };
const initialized = spawnSync('git', fixtureInitArgs(repo), { env: gitPaths.env, encoding: 'utf8', windowsHide: true });
assert.equal(initialized.status, 0, initialized.stderr);
const old = path.join(dir, 'included-hooks');
writeText(path.join(old, 'pre-commit'), '#!/bin/sh\n# somebody else\n');
const included = path.join(dir, 'included.gitconfig');
writeText(included, `[core]\n hooksPath = ${JSON.stringify(old.replace(/\\/g, '/'))}\n`);
const gitTopLevel = spawnSync('git', ['rev-parse', '--show-toplevel'], {
cwd: repo, env: gitPaths.env, encoding: 'utf8', windowsHide: true,
});
assert.equal(gitTopLevel.status, 0, gitTopLevel.stderr);
const heading = conditional
? `[includeIf "gitdir:${gitTopLevel.stdout.trim().replace(/\\/g, '/')}/"]`
: '[include]';
writeText(globalFile, `${heading}\n path = ${JSON.stringify(included.replace(/\\/g, '/'))}\n`);
assert.equal(hooksPath('global', gitPaths), old.replace(/\\/g, '/'), conditional ? 'conditional include at Git’s resolved worktree path' : 'ordinary include');
const preview = installCommitGate({ kitHome: KIT_ROOT, env, gitPaths, cwd: repo, dryRun: true });
assert.equal(preview.previous, old.replace(/\\/g, '/'));
const installed = installCommitGate({ kitHome: KIT_ROOT, env, gitPaths, cwd: repo });
assert.equal(installed.ok, true);
assert.equal(installed.previous, old.replace(/\\/g, '/'));
assert.equal(previousHooksPath(gitPaths), old.replace(/\\/g, '/'));
const state = readJson(env.RESEARCH_KIT_INSTALL_STATE);
assert.equal(state.previousHooksPath, old.replace(/\\/g, '/'));
const removed = removeCommitGate({ env, gitPaths });
assert.equal(removed.restored, old.replace(/\\/g, '/'));
assert.equal(hooksPath('global', gitPaths), old.replace(/\\/g, '/'));
}
});

// A no-hit result describes inspected files. Reaching the count at the final file is
// different from returning at that count while another path remains uninspected.
test('secret cap: coverage distinguishes a complete boundary from an uninspected path', () => {
Expand Down
Loading