@marcomigozzi/trailhead 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.it.md +158 -34
- package/README.md +147 -33
- package/bin/auto-intro-docs.test.js +72 -0
- package/bin/auto-merge-docs.test.js +33 -0
- package/bin/auto-merge-ticket-docs.test.js +32 -0
- package/bin/auto-review-rounds-docs.test.js +45 -0
- package/bin/auto-run-docs.test.js +60 -0
- package/bin/auto-scope-docs.test.js +33 -0
- package/bin/auto-slots-docs.test.js +45 -0
- package/bin/auto-uat-docs.test.js +47 -0
- package/bin/base-preflight-docs.test.js +65 -0
- package/bin/execute-split-docs.test.js +146 -0
- package/bin/grilling-holistic-review-docs.test.js +90 -0
- package/bin/isolation-answer-docs.test.js +41 -0
- package/bin/isolation-copy-docs.test.js +28 -0
- package/bin/lib/codex-projection.js +6 -6
- package/bin/lib/codex-projection.test.js +43 -2
- package/bin/needs-you-docs.test.js +41 -0
- package/bin/pick-stress-test-docs.test.js +105 -0
- package/bin/report-status-docs.test.js +195 -0
- package/bin/resolve-helpers-docs.test.js +36 -0
- package/bin/scope-confirm-skip-docs.test.js +70 -0
- package/bin/trailhead.js +6 -6
- package/bin/trailhead.test.js +355 -16
- package/bin/uat-setup-docs.test.js +65 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
- package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
- package/plugins/trailhead/agents/trailhead-debug.md +20 -1
- package/plugins/trailhead/agents/trailhead-executor.md +29 -2
- package/plugins/trailhead/agents/trailhead-fix.md +29 -2
- package/plugins/trailhead/agents/trailhead-plan.md +20 -1
- package/plugins/trailhead/agents/trailhead-research.md +22 -3
- package/plugins/trailhead/agents/trailhead-verify.md +15 -2
- package/plugins/trailhead/commands/auto.md +2 -2
- package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
- package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
- package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
- package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
- package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
- package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
- package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
- package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
- package/plugins/trailhead/hooks/lib/dashboard.test.js +472 -0
- package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
- package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
- package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
- package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
- package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
- package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +140 -0
- package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
- package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
- package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
- package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
- package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
- package/plugins/trailhead/hooks/mod/README.md +209 -36
- package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
- package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +304 -26
- package/plugins/trailhead/hooks/mod/map-pane.tsx +763 -160
- package/plugins/trailhead/hooks/mod/register.tsx +312 -40
- package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
- package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
- package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
- package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
- package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
- package/plugins/trailhead/hooks/mod/work-state-view.ts +38 -5
- package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
- package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
- package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
- package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
- package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +296 -22
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +253 -0
- package/plugins/trailhead/skills/_shared/choices.md +11 -4
- package/plugins/trailhead/skills/_shared/configuration-reference.md +17 -5
- package/plugins/trailhead/skills/_shared/load-first.md +3 -3
- package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +47 -14
- package/plugins/trailhead/skills/_shared/substrate.md +9 -4
- package/plugins/trailhead/skills/_shared/teamwork.md +17 -8
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +18 -10
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +12 -2
- package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +3 -3
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +9 -6
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +31 -18
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +29 -13
- package/plugins/trailhead/templates/statusline-trailhead.sh +77 -12
- package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
- package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
- package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
- package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
- package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
- package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
- package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
- package/plugins/trailhead/templates/trailhead-dashboard.js +360 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
- package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
- package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
- package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
- package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
- package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
- package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
- package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
- package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +2519 -382
- package/plugins/trailhead/tests/map-pane-view.test.ts +600 -18
- package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
- package/plugins/trailhead/tests/session-registry.test.ts +50 -0
- package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
- package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
- package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
- package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
- package/plugins/trailhead/tests/work-state-view.test.ts +44 -1
- package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
- package/plugins/trailhead/types/index.d.ts +62 -3
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tests for templates/trailhead-checkout-in.js. Run: node checkout-in.test.js
|
|
3
|
+
// No framework: plain asserts, same style as uat-backfill.test.js.
|
|
4
|
+
//
|
|
5
|
+
// Layers covered:
|
|
6
|
+
// 1. The pure token helpers: encode / decode round-trip.
|
|
7
|
+
// 2. The CLI against real temp git repos: invalid, here, move, the `cd` field.
|
|
8
|
+
// 3. The protocol contract: the work skill moves before it loads or preflights.
|
|
9
|
+
const assert = require('assert');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
const scriptPath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-checkout-in.js');
|
|
16
|
+
const skillPath = path.resolve(__dirname, '..', '..', 'skills', 'trailhead-work', 'SKILL.md');
|
|
17
|
+
const { encodeIn, decodeIn } = require(scriptPath);
|
|
18
|
+
|
|
19
|
+
let passed = 0;
|
|
20
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
21
|
+
const eq = (name, a, b) => { assert.deepStrictEqual(a, b, name); passed++; };
|
|
22
|
+
|
|
23
|
+
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'checkout-in-')));
|
|
24
|
+
const sh = (cwd, ...args) => {
|
|
25
|
+
const r = spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8' });
|
|
26
|
+
assert.strictEqual(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
|
|
27
|
+
return r.stdout;
|
|
28
|
+
};
|
|
29
|
+
function makeRepo(dir) {
|
|
30
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
31
|
+
sh(dir, 'init', '-q', '-b', 'main');
|
|
32
|
+
sh(dir, '-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '--allow-empty', '-m', 'init');
|
|
33
|
+
return fs.realpathSync(dir);
|
|
34
|
+
}
|
|
35
|
+
const run = (inArg, cwd) => {
|
|
36
|
+
const r = spawnSync(process.execPath, [scriptPath, '--in', inArg, '--cwd', cwd], { encoding: 'utf8' });
|
|
37
|
+
return { code: r.status, out: r.stdout.trim() ? JSON.parse(r.stdout.trim()) : null, err: r.stderr };
|
|
38
|
+
};
|
|
39
|
+
const tokenOf = (p) => encodeIn(p).slice('--in='.length);
|
|
40
|
+
|
|
41
|
+
// --- 1. pure helpers ---------------------------------------------------------------
|
|
42
|
+
const odd = [
|
|
43
|
+
'/tmp/with space/repo',
|
|
44
|
+
"/tmp/it's/repo",
|
|
45
|
+
'/tmp/"dq"/repo',
|
|
46
|
+
'/tmp/-leading/repo',
|
|
47
|
+
'-dash-first',
|
|
48
|
+
'/tmp/uniè中文/repo',
|
|
49
|
+
"/tmp/a'b'c d/$HOME/`x`",
|
|
50
|
+
];
|
|
51
|
+
for (const p of odd) {
|
|
52
|
+
const enc = encodeIn(p);
|
|
53
|
+
ok(`token shape ${p}`, /^--in=[A-Za-z0-9_-]+$/.test(enc));
|
|
54
|
+
eq(`round-trip ${p}`, decodeIn(enc.slice(5)), p);
|
|
55
|
+
}
|
|
56
|
+
eq('decode of garbage', decodeIn('!!!'), null);
|
|
57
|
+
eq('decode of empty', decodeIn(''), null);
|
|
58
|
+
|
|
59
|
+
// --- 2. CLI ------------------------------------------------------------------------
|
|
60
|
+
const main = makeRepo(path.join(tmp, 'main repo'));
|
|
61
|
+
const other = makeRepo(path.join(tmp, 'other'));
|
|
62
|
+
const plain = path.join(tmp, 'plain');
|
|
63
|
+
fs.mkdirSync(plain);
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
const r = run(tokenOf(main), main);
|
|
67
|
+
eq('here in the root', [r.code, r.out.status, r.out.path], [0, 'here', main]);
|
|
68
|
+
}
|
|
69
|
+
{
|
|
70
|
+
fs.mkdirSync(path.join(main, 'sub'));
|
|
71
|
+
const r = run(tokenOf(main), path.join(main, 'sub'));
|
|
72
|
+
eq('here from a subdir of the same checkout', r.out.status, 'here');
|
|
73
|
+
}
|
|
74
|
+
{
|
|
75
|
+
const r = run(tokenOf(main), other);
|
|
76
|
+
eq('move from another repo', [r.code, r.out.status, r.out.path], [0, 'move', main]);
|
|
77
|
+
ok('move carries a cd', r.out.cd === `cd -- '${main}'`);
|
|
78
|
+
}
|
|
79
|
+
{
|
|
80
|
+
const r = run(tokenOf(main), plain);
|
|
81
|
+
eq('move from outside any repo', r.out.status, 'move');
|
|
82
|
+
}
|
|
83
|
+
{
|
|
84
|
+
const r = run(tokenOf(main), path.join(tmp, 'missing-cwd'));
|
|
85
|
+
eq('move from a cwd that does not exist', r.out.status, 'move');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A worktree root is its own checkout: here only for itself.
|
|
89
|
+
{
|
|
90
|
+
const wt = path.join(tmp, 'wt');
|
|
91
|
+
sh(main, 'worktree', 'add', '-q', '-b', 'wt-branch', wt);
|
|
92
|
+
const wtReal = fs.realpathSync(wt);
|
|
93
|
+
eq('worktree root is here for itself', run(tokenOf(wtReal), wtReal).out.status, 'here');
|
|
94
|
+
eq('worktree root is move from the main checkout', run(tokenOf(wtReal), main).out.status, 'move');
|
|
95
|
+
eq('main checkout is move from the worktree', run(tokenOf(main), wtReal).out.status, 'move');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A symlinked path compares canonical.
|
|
99
|
+
{
|
|
100
|
+
const link = path.join(tmp, 'link-to-main');
|
|
101
|
+
fs.symlinkSync(main, link);
|
|
102
|
+
const r = run(tokenOf(link), main);
|
|
103
|
+
eq('symlinked path is here by canonical compare', r.out.status, 'here');
|
|
104
|
+
eq('symlinked cwd is here too', run(tokenOf(main), link).out.status, 'here');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// invalid: never searches for another checkout.
|
|
108
|
+
{
|
|
109
|
+
eq('relative path is invalid', run(tokenOf('relative/dir'), main).out.status, 'invalid');
|
|
110
|
+
eq('missing path is invalid', run(tokenOf(path.join(tmp, 'nope')), main).out.status, 'invalid');
|
|
111
|
+
eq('non-repo dir is invalid', run(tokenOf(plain), main).out.status, 'invalid');
|
|
112
|
+
eq('subdir of a repo is invalid (not a root)', run(tokenOf(path.join(main, 'sub')), other).out.status, 'invalid');
|
|
113
|
+
const bad = run('!!!', main);
|
|
114
|
+
eq('undecodable token is invalid', [bad.code, bad.out.status], [0, 'invalid']);
|
|
115
|
+
ok('invalid has a reason', typeof bad.out.reason === 'string' && bad.out.reason.length > 0);
|
|
116
|
+
ok('invalid has no cd', !('cd' in run(tokenOf(plain), main).out));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// usage errors
|
|
120
|
+
{
|
|
121
|
+
const r = spawnSync(process.execPath, [scriptPath, '--cwd', main], { encoding: 'utf8' });
|
|
122
|
+
eq('missing --in is a usage error', r.status, 2);
|
|
123
|
+
const r2 = spawnSync(process.execPath, [scriptPath, '--in', tokenOf(main)], { encoding: 'utf8' });
|
|
124
|
+
eq('missing --cwd is a usage error', r2.status, 2);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The cd field, run by a shell, lands in the path for every pathological case.
|
|
128
|
+
{
|
|
129
|
+
let n = 0;
|
|
130
|
+
for (const p of odd) {
|
|
131
|
+
if (!path.isAbsolute(p)) continue;
|
|
132
|
+
const dir = path.join(tmp, `odd${n++}`, p.replace(/^\/tmp\//, '').replace(/\//g, '_'));
|
|
133
|
+
const real = makeRepo(dir);
|
|
134
|
+
const r = run(tokenOf(real), plain);
|
|
135
|
+
eq(`move for ${dir}`, r.out.status, 'move');
|
|
136
|
+
const cd = spawnSync('sh', ['-c', `${r.out.cd} && pwd -P`], { encoding: 'utf8', cwd: plain });
|
|
137
|
+
eq(`cd lands in ${dir}`, cd.stdout.trim(), real);
|
|
138
|
+
}
|
|
139
|
+
// A quote and a leading dash in the actual directory names.
|
|
140
|
+
for (const name of ["it's here", '-dash', 'sp ace "dq" $x `y`']) {
|
|
141
|
+
const real = makeRepo(path.join(tmp, 'pathological', name));
|
|
142
|
+
const r = run(tokenOf(real), plain);
|
|
143
|
+
const cd = spawnSync('sh', ['-c', `${r.out.cd} && pwd -P`], { encoding: 'utf8', cwd: plain });
|
|
144
|
+
eq(`cd lands in ${name}`, cd.stdout.trim(), real);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- 3. protocol contract: the move comes before anything else -----------------------
|
|
149
|
+
{
|
|
150
|
+
const skill = fs.readFileSync(skillPath, 'utf8');
|
|
151
|
+
const move = skill.indexOf('## Move into the handed checkout');
|
|
152
|
+
ok('move section exists', move >= 0);
|
|
153
|
+
const load = skill.indexOf('## Load first');
|
|
154
|
+
const pre = skill.indexOf('## Preflight');
|
|
155
|
+
ok('move section sits before Load first', load > move);
|
|
156
|
+
ok('move section sits before the Preflight', pre > move);
|
|
157
|
+
const section = skill.slice(move, load);
|
|
158
|
+
ok('names the helper', section.includes('trailhead-checkout-in.js'));
|
|
159
|
+
ok('names the --in= word', section.includes('--in='));
|
|
160
|
+
ok('names work and resume', /`work`/.test(section) && /`resume`/.test(section));
|
|
161
|
+
ok('runs the cd field verbatim', /`cd` field[^.]*verbatim/.test(section));
|
|
162
|
+
ok('cd is its own Bash call', /own Bash call/.test(section));
|
|
163
|
+
ok('post-move re-check requires here', /re-run the helper/.test(section) && /`here`/.test(section));
|
|
164
|
+
ok('stops on invalid', /`invalid`[^.]*stop/.test(section));
|
|
165
|
+
ok('stops on a failed cd', /failed `cd`[^.]*stop|`cd` fails[^.]*stop|stop[^.]*`cd` fail/i.test(section));
|
|
166
|
+
ok('stops when the post-check is not here', /could not move/.test(section));
|
|
167
|
+
ok('stops when the helper is missing', /missing[^.]*stop|stop[^.]*missing/i.test(section));
|
|
168
|
+
ok('never moves by hand', /by hand/.test(section));
|
|
169
|
+
ok('Routing points at the move section', /Routing[\s\S]*Move into the handed checkout/.test(skill.slice(skill.indexOf('## Routing'))));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
173
|
+
console.log(`checkout-in: ${passed} assertions passed`);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// commit-message-check.js: the single source of truth for trailhead's commit
|
|
3
3
|
// message rules, shared by two guardrails so they can never drift:
|
|
4
4
|
// - trailhead-commit-guard.js (Claude Code PreToolUse(Bash) hook)
|
|
5
|
-
// - templates/trailhead-commit-msg
|
|
5
|
+
// - templates/trailhead-commit-msg.cjs (host-independent git commit-msg hook logic,
|
|
6
6
|
// installed into .git/hooks at repo first-use so it runs on ANY host)
|
|
7
7
|
// Two rules: NEVER a Co-Authored-By trailer; a Conventional Commits subject
|
|
8
8
|
// (<type>(scope)?: subject) of at most 72 chars. Plus a conditional third rule:
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Drives the SAME fixtures through two paths:
|
|
6
6
|
// 1. checkCommitMessage() directly (the source of truth).
|
|
7
|
-
// 2. the template hook (templates/trailhead-commit-msg
|
|
7
|
+
// 2. the template hook (templates/trailhead-commit-msg sh wrapper, which runs
|
|
8
|
+
// the sibling trailhead-commit-msg.cjs logic) end-to-end via
|
|
8
9
|
// execFileSync, feeding it the message as a temp file, run from a fresh
|
|
9
10
|
// mkdtemp cwd (so the template's own .trailhead/session-ticket lookup is
|
|
10
11
|
// independent of this repo's real marker).
|
|
@@ -60,7 +61,7 @@ function runTemplateHook(message, marker) {
|
|
|
60
61
|
let allowed = true;
|
|
61
62
|
let stderr = '';
|
|
62
63
|
try {
|
|
63
|
-
execFileSync('
|
|
64
|
+
execFileSync('sh', [templatePath, tmpFile], { cwd: tmp, stdio: 'pipe' });
|
|
64
65
|
} catch (e) {
|
|
65
66
|
allowed = false;
|
|
66
67
|
stderr = e.stderr ? e.stderr.toString() : '';
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regression for #257: the git commit-msg hook must work in a repo whose root
|
|
3
|
+
// package.json says "type": "module". The hook is installed by the sync script
|
|
4
|
+
// into a fixture repo and exercised through real `git commit` calls.
|
|
5
|
+
// Run: node commit-msg-module-repo.test.js
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const syncScript = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-commit-msg-sync.js');
|
|
13
|
+
|
|
14
|
+
// Prefisso della riga che il wrapper stampa quando non può applicare le regole (#262).
|
|
15
|
+
const INACTIVE = 'trailhead: commit-msg hook inactive (';
|
|
16
|
+
|
|
17
|
+
let passed = 0;
|
|
18
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
19
|
+
|
|
20
|
+
function gitEnv(extra) {
|
|
21
|
+
return Object.assign({}, process.env, {
|
|
22
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
23
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
24
|
+
GIT_AUTHOR_NAME: 'Trailhead Test',
|
|
25
|
+
GIT_AUTHOR_EMAIL: 'trailhead-test@example.com',
|
|
26
|
+
GIT_COMMITTER_NAME: 'Trailhead Test',
|
|
27
|
+
GIT_COMMITTER_EMAIL: 'trailhead-test@example.com',
|
|
28
|
+
}, extra || {});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Repo fixture: package.json "type": "module" alla radice, hook installato dallo
|
|
32
|
+
// script di sync, marker session-ticket = #1.
|
|
33
|
+
function makeRepo() {
|
|
34
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trailhead-commit-msg-esm-'));
|
|
35
|
+
execFileSync('git', ['init', '-q'], { cwd: dir, env: gitEnv() });
|
|
36
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ type: 'module' }));
|
|
37
|
+
const out = execFileSync(process.execPath, [syncScript, '--repo', dir], { env: gitEnv(), encoding: 'utf8' });
|
|
38
|
+
assert.strictEqual(JSON.parse(out).status, 'installed');
|
|
39
|
+
fs.mkdirSync(path.join(dir, '.trailhead'));
|
|
40
|
+
fs.writeFileSync(path.join(dir, '.trailhead', 'session-ticket'), '#1 x\n');
|
|
41
|
+
return { dir, hooksDir: path.join(dir, '.git', 'hooks') };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function commit(dir, message) {
|
|
45
|
+
const r = spawnSync('git', ['commit', '--allow-empty', '-q', '-m', message], { cwd: dir, env: gitEnv(), encoding: 'utf8' });
|
|
46
|
+
return { status: r.status, stderr: r.stderr || '' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// (a) commit convenzionale con Refs: #1 passa
|
|
50
|
+
{
|
|
51
|
+
const { dir } = makeRepo();
|
|
52
|
+
const r = commit(dir, 'fix: thing\n\nRefs: #1');
|
|
53
|
+
ok('module repo: conventional commit with Refs passes', r.status === 0);
|
|
54
|
+
ok('module repo: clean commit stays silent', !r.stderr.includes('trailhead:'));
|
|
55
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// (b) senza trailer: rifiutato col messaggio di trailhead
|
|
59
|
+
{
|
|
60
|
+
const { dir } = makeRepo();
|
|
61
|
+
const r = commit(dir, 'fix: thing');
|
|
62
|
+
ok('module repo: missing Refs is rejected with exit 1', r.status === 1);
|
|
63
|
+
ok('module repo: rejection carries trailhead message', r.stderr.includes('trailhead:') && r.stderr.includes('Refs: #1'));
|
|
64
|
+
ok('module repo: no ESM load error leaks', !r.stderr.includes('require is not defined'));
|
|
65
|
+
ok('module repo: a violation carries no inactive notice', !r.stderr.includes(INACTIVE));
|
|
66
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// (c) errori di avvio: mai bloccare un commit, ma segnalarlo su stderr (#262)
|
|
70
|
+
{
|
|
71
|
+
const { dir, hooksDir } = makeRepo();
|
|
72
|
+
fs.rmSync(path.join(hooksDir, 'trailhead-commit-msg.cjs'));
|
|
73
|
+
const r = commit(dir, 'not conventional at all');
|
|
74
|
+
ok('module repo: missing .cjs lets the commit through', r.status === 0);
|
|
75
|
+
ok('module repo: missing .cjs prints the inactive notice',
|
|
76
|
+
r.stderr.includes(INACTIVE + 'trailhead-commit-msg.cjs missing or unreadable)'));
|
|
77
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
// .cjs presente ma illeggibile: da root chmod non toglie la lettura, quindi si salta.
|
|
80
|
+
if (typeof process.getuid !== 'function' || process.getuid() !== 0) {
|
|
81
|
+
const { dir, hooksDir } = makeRepo();
|
|
82
|
+
fs.chmodSync(path.join(hooksDir, 'trailhead-commit-msg.cjs'), 0o000);
|
|
83
|
+
const r = commit(dir, 'not conventional at all');
|
|
84
|
+
ok('module repo: unreadable .cjs lets the commit through', r.status === 0);
|
|
85
|
+
ok('module repo: unreadable .cjs prints the inactive notice',
|
|
86
|
+
r.stderr.includes(INACTIVE + 'trailhead-commit-msg.cjs missing or unreadable)'));
|
|
87
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
{
|
|
90
|
+
const { dir, hooksDir } = makeRepo();
|
|
91
|
+
fs.writeFileSync(path.join(hooksDir, 'trailhead-commit-msg.cjs'), "throw new Error('boom at load');\n");
|
|
92
|
+
const r = commit(dir, 'not conventional at all');
|
|
93
|
+
ok('module repo: .cjs crashing at load lets the commit through', r.status === 0);
|
|
94
|
+
ok('module repo: .cjs crashing at load prints the inactive notice',
|
|
95
|
+
r.stderr.includes(INACTIVE + 'trailhead-commit-msg.cjs failed to run, exit '));
|
|
96
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
{
|
|
99
|
+
// node assente dal PATH: il wrapper deve uscire 0 anche con un messaggio che
|
|
100
|
+
// altrimenti sarebbe una violazione (controllo negativo con PATH normale).
|
|
101
|
+
const { dir, hooksDir } = makeRepo();
|
|
102
|
+
const msgFile = path.join(dir, 'MSG');
|
|
103
|
+
fs.writeFileSync(msgFile, 'not conventional at all\n');
|
|
104
|
+
const emptyBin = fs.mkdtempSync(path.join(os.tmpdir(), 'trailhead-empty-bin-'));
|
|
105
|
+
const hook = path.join(hooksDir, 'commit-msg');
|
|
106
|
+
const withNode = spawnSync('/bin/sh', [hook, msgFile], { cwd: dir, env: gitEnv(), encoding: 'utf8' });
|
|
107
|
+
ok('wrapper control: violation exits 1 with node on PATH', withNode.status === 1);
|
|
108
|
+
const noNode = spawnSync('/bin/sh', [hook, msgFile], { cwd: dir, env: { PATH: emptyBin }, encoding: 'utf8' });
|
|
109
|
+
ok('wrapper: node absent from PATH exits 0', noNode.status === 0);
|
|
110
|
+
ok('wrapper: node absent from PATH prints the inactive notice',
|
|
111
|
+
(noNode.stderr || '').includes(INACTIVE + 'node not found on PATH)'));
|
|
112
|
+
fs.rmSync(emptyBin, { recursive: true, force: true });
|
|
113
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`✓ commit-msg module repo (#257): ${passed} assertions passed`);
|
|
@@ -16,7 +16,10 @@ const os = require('os');
|
|
|
16
16
|
|
|
17
17
|
const scriptPath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-commit-msg-sync.js');
|
|
18
18
|
const templatePath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-commit-msg');
|
|
19
|
+
const cjsTemplatePath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-commit-msg.cjs');
|
|
19
20
|
const oldFixturePath = path.join(__dirname, 'fixtures', 'commit-msg-a319156');
|
|
21
|
+
// Hook JS monofile pre-#257 (extensionless CommonJS), da migrare al nuovo layout.
|
|
22
|
+
const legacyJsFixturePath = path.join(__dirname, 'fixtures', 'commit-msg-legacy-js');
|
|
20
23
|
|
|
21
24
|
const {
|
|
22
25
|
resolveHooksTarget,
|
|
@@ -29,7 +32,10 @@ let passed = 0;
|
|
|
29
32
|
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
30
33
|
|
|
31
34
|
const templateText = fs.readFileSync(templatePath, 'utf8');
|
|
35
|
+
const cjsText = fs.readFileSync(cjsTemplatePath, 'utf8');
|
|
32
36
|
const oldHookText = fs.readFileSync(oldFixturePath, 'utf8');
|
|
37
|
+
const legacyJsText = fs.readFileSync(legacyJsFixturePath, 'utf8');
|
|
38
|
+
const CJS_NAME = 'trailhead-commit-msg.cjs';
|
|
33
39
|
|
|
34
40
|
function mktmp() {
|
|
35
41
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'trailhead-commit-msg-sync-test-'));
|
|
@@ -38,6 +44,9 @@ function mktmp() {
|
|
|
38
44
|
// --- isTrailheadHook() unit cases -------------------------------------------
|
|
39
45
|
ok('isTrailheadHook: current template is recognised', isTrailheadHook(templateText));
|
|
40
46
|
ok('isTrailheadHook: old (a319156) hook is recognised', isTrailheadHook(oldHookText));
|
|
47
|
+
ok('isTrailheadHook: legacy single-file JS hook is recognised', isTrailheadHook(legacyJsText));
|
|
48
|
+
ok('isTrailheadHook: wrapper header is on a "#" line within the first 5', isTrailheadHook(templateText));
|
|
49
|
+
ok('isTrailheadHook: wrapper is a sh script', templateText.startsWith('#!/bin/sh\n'));
|
|
41
50
|
ok('isTrailheadHook: plain foreign script is not', !isTrailheadHook('#!/bin/sh\necho hi\n'));
|
|
42
51
|
ok('isTrailheadHook: mention outside the first 5 lines, not line-start, is not',
|
|
43
52
|
!isTrailheadHook('#!/bin/sh\necho hi\necho more\necho stuff\necho again\n# trailhead commit-msg hook (git). fake\n'));
|
|
@@ -55,13 +64,14 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
|
|
|
55
64
|
// absent -> installed, 0755
|
|
56
65
|
{
|
|
57
66
|
const dir = mktmp();
|
|
58
|
-
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
|
|
67
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
|
|
59
68
|
const hookPath = path.join(dir, 'commit-msg');
|
|
60
69
|
ok('syncCommitMsgHook: absent -> installed', result.status === 'installed');
|
|
61
70
|
ok('syncCommitMsgHook: absent -> hook path returned', result.hook === hookPath);
|
|
62
71
|
ok('syncCommitMsgHook: absent -> file written', fs.existsSync(hookPath));
|
|
63
72
|
ok('syncCommitMsgHook: absent -> content matches template', fs.readFileSync(hookPath, 'utf8') === templateText);
|
|
64
73
|
ok('syncCommitMsgHook: absent -> mode 0755', modeOf(hookPath) === 0o755);
|
|
74
|
+
ok('syncCommitMsgHook: absent -> .cjs written beside it', fs.readFileSync(path.join(dir, CJS_NAME), 'utf8') === cjsText);
|
|
65
75
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
66
76
|
}
|
|
67
77
|
|
|
@@ -71,21 +81,77 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
|
|
|
71
81
|
const hookPath = path.join(dir, 'commit-msg');
|
|
72
82
|
fs.writeFileSync(hookPath, oldHookText);
|
|
73
83
|
fs.chmodSync(hookPath, 0o755);
|
|
74
|
-
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
|
|
84
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
|
|
75
85
|
ok('syncCommitMsgHook: old -> upgraded', result.status === 'upgraded');
|
|
76
86
|
ok('syncCommitMsgHook: old -> bytes now match template', fs.readFileSync(hookPath, 'utf8') === templateText);
|
|
77
87
|
ok('syncCommitMsgHook: old -> mode 0755', modeOf(hookPath) === 0o755);
|
|
78
88
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
79
89
|
}
|
|
80
90
|
|
|
91
|
+
// legacy single-file JS hook -> upgraded to wrapper + .cjs
|
|
92
|
+
{
|
|
93
|
+
const dir = mktmp();
|
|
94
|
+
const hookPath = path.join(dir, 'commit-msg');
|
|
95
|
+
fs.writeFileSync(hookPath, legacyJsText);
|
|
96
|
+
fs.chmodSync(hookPath, 0o755);
|
|
97
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, cjsText, hooksPathSet: false, fs });
|
|
98
|
+
ok('syncCommitMsgHook: legacy JS -> upgraded', result.status === 'upgraded');
|
|
99
|
+
ok('syncCommitMsgHook: legacy JS -> wrapper now matches template', fs.readFileSync(hookPath, 'utf8') === templateText);
|
|
100
|
+
ok('syncCommitMsgHook: legacy JS -> .cjs written', fs.readFileSync(path.join(dir, CJS_NAME), 'utf8') === cjsText);
|
|
101
|
+
ok('syncCommitMsgHook: legacy JS -> wrapper mode 0755', modeOf(hookPath) === 0o755);
|
|
102
|
+
const again = syncCommitMsgHook({ hooksDir: dir, templateText, cjsText, hooksPathSet: false, fs });
|
|
103
|
+
ok('syncCommitMsgHook: legacy JS -> second run is current', again.status === 'current');
|
|
104
|
+
ok('syncCommitMsgHook: legacy JS -> no temp files left', fs.readdirSync(dir).sort().join(',') === `commit-msg,${CJS_NAME}`);
|
|
105
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// current wrapper but outdated .cjs -> upgraded, .cjs rewritten
|
|
109
|
+
{
|
|
110
|
+
const dir = mktmp();
|
|
111
|
+
const hookPath = path.join(dir, 'commit-msg');
|
|
112
|
+
fs.writeFileSync(hookPath, templateText);
|
|
113
|
+
fs.chmodSync(hookPath, 0o755);
|
|
114
|
+
fs.writeFileSync(path.join(dir, CJS_NAME), '// stale\n');
|
|
115
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, cjsText, hooksPathSet: false, fs });
|
|
116
|
+
ok('syncCommitMsgHook: outdated .cjs -> upgraded', result.status === 'upgraded');
|
|
117
|
+
ok('syncCommitMsgHook: outdated .cjs -> rewritten', fs.readFileSync(path.join(dir, CJS_NAME), 'utf8') === cjsText);
|
|
118
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// current wrapper but .cjs missing -> upgraded, .cjs restored
|
|
122
|
+
{
|
|
123
|
+
const dir = mktmp();
|
|
124
|
+
const hookPath = path.join(dir, 'commit-msg');
|
|
125
|
+
fs.writeFileSync(hookPath, templateText);
|
|
126
|
+
fs.chmodSync(hookPath, 0o755);
|
|
127
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, cjsText, hooksPathSet: false, fs });
|
|
128
|
+
ok('syncCommitMsgHook: missing .cjs -> upgraded', result.status === 'upgraded');
|
|
129
|
+
ok('syncCommitMsgHook: missing .cjs -> restored', fs.readFileSync(path.join(dir, CJS_NAME), 'utf8') === cjsText);
|
|
130
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// outdated wrapper (older sh wrapper text) + current .cjs -> upgraded wrapper
|
|
134
|
+
{
|
|
135
|
+
const dir = mktmp();
|
|
136
|
+
const hookPath = path.join(dir, 'commit-msg');
|
|
137
|
+
fs.writeFileSync(hookPath, '#!/bin/sh\n# trailhead commit-msg hook (git). older wrapper\nexit 0\n');
|
|
138
|
+
fs.chmodSync(hookPath, 0o755);
|
|
139
|
+
fs.writeFileSync(path.join(dir, CJS_NAME), cjsText);
|
|
140
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, cjsText, hooksPathSet: false, fs });
|
|
141
|
+
ok('syncCommitMsgHook: outdated wrapper -> upgraded', result.status === 'upgraded');
|
|
142
|
+
ok('syncCommitMsgHook: outdated wrapper -> now matches template', fs.readFileSync(hookPath, 'utf8') === templateText);
|
|
143
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
|
|
81
146
|
// CRLF current -> current, untouched
|
|
82
147
|
{
|
|
83
148
|
const dir = mktmp();
|
|
84
149
|
const hookPath = path.join(dir, 'commit-msg');
|
|
85
150
|
const crlfTemplate = templateText.replace(/\n/g, '\r\n');
|
|
151
|
+
fs.writeFileSync(path.join(dir, CJS_NAME), cjsText);
|
|
86
152
|
fs.writeFileSync(hookPath, crlfTemplate);
|
|
87
153
|
fs.chmodSync(hookPath, 0o755);
|
|
88
|
-
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
|
|
154
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
|
|
89
155
|
ok('syncCommitMsgHook: CRLF current -> current', result.status === 'current');
|
|
90
156
|
ok('syncCommitMsgHook: CRLF current -> untouched bytes', fs.readFileSync(hookPath, 'utf8') === crlfTemplate);
|
|
91
157
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -96,8 +162,9 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
|
|
|
96
162
|
const dir = mktmp();
|
|
97
163
|
const hookPath = path.join(dir, 'commit-msg');
|
|
98
164
|
fs.writeFileSync(hookPath, templateText);
|
|
165
|
+
fs.writeFileSync(path.join(dir, CJS_NAME), cjsText);
|
|
99
166
|
fs.chmodSync(hookPath, 0o644);
|
|
100
|
-
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
|
|
167
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
|
|
101
168
|
ok('syncCommitMsgHook: current 0644 -> status current', result.status === 'current');
|
|
102
169
|
ok('syncCommitMsgHook: current 0644 -> mode fixed to 0755', modeOf(hookPath) === 0o755);
|
|
103
170
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -110,10 +177,11 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
|
|
|
110
177
|
const foreign = '#!/bin/sh\necho hi\n';
|
|
111
178
|
fs.writeFileSync(hookPath, foreign);
|
|
112
179
|
fs.chmodSync(hookPath, 0o755);
|
|
113
|
-
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
|
|
180
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
|
|
114
181
|
ok('syncCommitMsgHook: foreign shell -> foreign', result.status === 'foreign');
|
|
115
182
|
ok('syncCommitMsgHook: foreign shell -> bytes untouched', fs.readFileSync(hookPath, 'utf8') === foreign);
|
|
116
183
|
ok('syncCommitMsgHook: foreign shell -> mode untouched', modeOf(hookPath) === 0o755);
|
|
184
|
+
ok('syncCommitMsgHook: foreign shell -> no .cjs written', !fs.existsSync(path.join(dir, CJS_NAME)));
|
|
117
185
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
118
186
|
}
|
|
119
187
|
|
|
@@ -124,7 +192,7 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
|
|
|
124
192
|
const foreign = '#!/bin/sh\necho "trailhead commit-msg hook (git). not really"\nexit 0\n';
|
|
125
193
|
fs.writeFileSync(hookPath, foreign);
|
|
126
194
|
fs.chmodSync(hookPath, 0o755);
|
|
127
|
-
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
|
|
195
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
|
|
128
196
|
ok('syncCommitMsgHook: foreign wrapper mentioning phrase -> foreign', result.status === 'foreign');
|
|
129
197
|
ok('syncCommitMsgHook: foreign wrapper -> bytes untouched', fs.readFileSync(hookPath, 'utf8') === foreign);
|
|
130
198
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -138,10 +206,11 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
|
|
|
138
206
|
const targetContent = '#!/bin/sh\necho real\n';
|
|
139
207
|
fs.writeFileSync(targetPath, targetContent);
|
|
140
208
|
fs.symlinkSync(targetPath, hookPath);
|
|
141
|
-
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, fs });
|
|
209
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: false, cjsText, fs });
|
|
142
210
|
ok('syncCommitMsgHook: symlink -> foreign', result.status === 'foreign');
|
|
143
211
|
ok('syncCommitMsgHook: symlink -> still a symlink', fs.lstatSync(hookPath).isSymbolicLink());
|
|
144
212
|
ok('syncCommitMsgHook: symlink target untouched', fs.readFileSync(targetPath, 'utf8') === targetContent);
|
|
213
|
+
ok('syncCommitMsgHook: symlink -> no .cjs written', !fs.existsSync(path.join(dir, CJS_NAME)));
|
|
145
214
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
146
215
|
}
|
|
147
216
|
|
|
@@ -149,9 +218,9 @@ function modeOf(p) { return fs.statSync(p).mode & 0o777; }
|
|
|
149
218
|
{
|
|
150
219
|
const dir = mktmp();
|
|
151
220
|
const hookPath = path.join(dir, 'commit-msg');
|
|
152
|
-
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: true, fs });
|
|
221
|
+
const result = syncCommitMsgHook({ hooksDir: dir, templateText, hooksPathSet: true, cjsText, fs });
|
|
153
222
|
ok('syncCommitMsgHook: absent + hooksPathSet -> skipped', result.status === 'skipped');
|
|
154
|
-
ok('syncCommitMsgHook: absent + hooksPathSet -> nothing created', !fs.existsSync(hookPath));
|
|
223
|
+
ok('syncCommitMsgHook: absent + hooksPathSet -> nothing created', !fs.existsSync(hookPath) && !fs.existsSync(path.join(dir, CJS_NAME)));
|
|
155
224
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
156
225
|
}
|
|
157
226
|
|
|
@@ -226,6 +295,7 @@ function assertSingleJsonLine(out, label) {
|
|
|
226
295
|
const hookPath = path.join(dir, '.git', 'hooks', 'commit-msg');
|
|
227
296
|
ok('e2e fresh repo: hook file exists', fs.existsSync(hookPath));
|
|
228
297
|
ok('e2e fresh repo: hook is executable', (modeOf(hookPath) & 0o111) !== 0);
|
|
298
|
+
ok('e2e fresh repo: .cjs installed beside it', fs.readFileSync(path.join(dir, '.git', 'hooks', CJS_NAME), 'utf8') === cjsText);
|
|
229
299
|
|
|
230
300
|
const out2 = runScript(['--repo', dir], { env: gitEnv() });
|
|
231
301
|
const parsed2 = assertSingleJsonLine(out2, 'e2e fresh repo (2nd run)');
|
|
@@ -246,6 +316,24 @@ function assertSingleJsonLine(out, label) {
|
|
|
246
316
|
const parsed = assertSingleJsonLine(out, 'e2e seeded old hook');
|
|
247
317
|
ok('e2e seeded old hook: status upgraded', parsed.status === 'upgraded');
|
|
248
318
|
ok('e2e seeded old hook: content now matches template', fs.readFileSync(hookPath, 'utf8') === templateText);
|
|
319
|
+
ok('e2e seeded old hook: .cjs installed', fs.readFileSync(path.join(hooksDir, CJS_NAME), 'utf8') === cjsText);
|
|
320
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// seeded legacy single-file JS hook -> upgraded to wrapper + .cjs
|
|
324
|
+
{
|
|
325
|
+
const dir = mktmp();
|
|
326
|
+
execFileSync('git', ['init', '-q'], { cwd: dir, env: gitEnv() });
|
|
327
|
+
const hooksDir = path.join(dir, '.git', 'hooks');
|
|
328
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
329
|
+
const hookPath = path.join(hooksDir, 'commit-msg');
|
|
330
|
+
fs.writeFileSync(hookPath, legacyJsText);
|
|
331
|
+
fs.chmodSync(hookPath, 0o755);
|
|
332
|
+
const out = runScript(['--repo', dir], { env: gitEnv() });
|
|
333
|
+
const parsed = assertSingleJsonLine(out, 'e2e seeded legacy JS hook');
|
|
334
|
+
ok('e2e seeded legacy JS hook: status upgraded', parsed.status === 'upgraded');
|
|
335
|
+
ok('e2e seeded legacy JS hook: wrapper installed', fs.readFileSync(hookPath, 'utf8') === templateText);
|
|
336
|
+
ok('e2e seeded legacy JS hook: .cjs installed', fs.readFileSync(path.join(hooksDir, CJS_NAME), 'utf8') === cjsText);
|
|
249
337
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
250
338
|
}
|
|
251
339
|
|
|
@@ -263,6 +351,7 @@ function assertSingleJsonLine(out, label) {
|
|
|
263
351
|
const parsed = assertSingleJsonLine(out, 'e2e seeded foreign hook');
|
|
264
352
|
ok('e2e seeded foreign hook: status foreign', parsed.status === 'foreign');
|
|
265
353
|
ok('e2e seeded foreign hook: bytes untouched', fs.readFileSync(hookPath, 'utf8') === foreign);
|
|
354
|
+
ok('e2e seeded foreign hook: no .cjs written', !fs.existsSync(path.join(hooksDir, CJS_NAME)));
|
|
266
355
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
267
356
|
}
|
|
268
357
|
|
|
@@ -293,6 +382,7 @@ function assertSingleJsonLine(out, label) {
|
|
|
293
382
|
const parsed = assertSingleJsonLine(out, 'e2e core.hooksPath old hook');
|
|
294
383
|
ok('e2e core.hooksPath old hook: status upgraded', parsed.status === 'upgraded');
|
|
295
384
|
ok('e2e core.hooksPath old hook: upgraded in .githooks', fs.readFileSync(hookPath, 'utf8') === templateText);
|
|
385
|
+
ok('e2e core.hooksPath old hook: .cjs lands in .githooks', fs.readFileSync(path.join(githooksDir, CJS_NAME), 'utf8') === cjsText);
|
|
296
386
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
297
387
|
}
|
|
298
388
|
|
|
@@ -311,6 +401,7 @@ function assertSingleJsonLine(out, label) {
|
|
|
311
401
|
const parsed = assertSingleJsonLine(out, 'e2e core.hooksPath foreign hook');
|
|
312
402
|
ok('e2e core.hooksPath foreign hook: status foreign', parsed.status === 'foreign');
|
|
313
403
|
ok('e2e core.hooksPath foreign hook: untouched', fs.readFileSync(hookPath, 'utf8') === foreign);
|
|
404
|
+
ok('e2e core.hooksPath foreign hook: no .cjs written', !fs.existsSync(path.join(githooksDir, CJS_NAME)));
|
|
314
405
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
315
406
|
}
|
|
316
407
|
|