diff --git a/scripts/process-gate.mjs b/scripts/process-gate.mjs index c39393a0..908f1df8 100644 --- a/scripts/process-gate.mjs +++ b/scripts/process-gate.mjs @@ -575,13 +575,20 @@ function main(argv) { let range = value('range'); if (!range && flag('github-range')) { + // gitObjectExists ловит исключение, чтобы подменить осиротевший после + // force-push BEFORE_SHA на origin/dev (#315). Раннер обязан бросать, + // а не завершать процесс, как это делает git() ниже. range = resolveValidationRange({ eventName: process.env.EVENT_NAME, beforeSha: process.env.BEFORE_SHA, baseSha: process.env.BASE_SHA, headSha: process.env.HEAD_SHA, developmentBranch: process.env.DEVELOPMENT_BRANCH, - }, (args) => git(args, repo).trim()); + }, (args) => { + const r = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8' }); + if (r.status !== 0) throw new Error(`git ${args.join(' ')} → ${(r.stderr || '').trim()}`); + return r.stdout.trim(); + }); } if (!range) { const hasDev = spawnSync('git', ['-C', repo, 'rev-parse', '--verify', 'origin/dev'], diff --git a/test/process-gate.test.mjs b/test/process-gate.test.mjs index 77ea842b..978b0dfa 100644 --- a/test/process-gate.test.mjs +++ b/test/process-gate.test.mjs @@ -535,6 +535,64 @@ test('the CLI judges a rebased issue branch by its own commits (#190)', (t) => { } }); +test('the CLI falls back to origin/dev when BEFORE_SHA is orphaned by a force-push (#315)', (t) => { + const probe = spawnSync('git', ['--version'], { encoding: 'utf8' }); + if (probe.status !== 0) { + t.skip('git недоступен'); + return; + } + const dir = mkdtempSync(join(tmpdir(), 'hp-gate-315-')); + const gate = fileURLToPath(new URL('../scripts/process-gate.mjs', import.meta.url)); + const git = (...args) => { + const r = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8' }); + assert.equal(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`); + return r.stdout; + }; + const write = (rel, text) => { + const full = join(dir, rel); + mkdirSync(join(full, '..'), { recursive: true }); + writeFileSync(full, text); + }; + const commitAll = (message) => { + git('add', '-A'); + git('-c', 'user.name=t', '-c', 'user.email=t@t', '-c', 'core.hooksPath=/dev/null', + 'commit', '-q', '-m', message); + }; + + try { + git('init', '-q', '-b', 'dev'); + write('README.md', 'base\n'); + commitAll('Base'); + git('update-ref', 'refs/remotes/origin/dev', git('rev-parse', 'HEAD').trim()); + + git('checkout', '-q', '-b', 'issue/7-own-work'); + write('scripts/w.mjs', 'export const w = 1;\n'); + commitAll('Own work\n\nIssue: #7\nUser-Visible: no'); + + // Push-событие после force-push: BEFORE_SHA указывает на переписанную + // вершину, которой в клоне больше нет. Раньше первый же cat-file убивал + // процесс кодом 2; теперь диапазон берётся от merge-base с origin/dev. + const r = spawnSync(process.execPath, [gate, '--repo', dir, '--github-range', '--json'], { + encoding: 'utf8', + env: { + ...process.env, + EVENT_NAME: 'push', + BEFORE_SHA: 'f'.repeat(40), + BASE_SHA: '', + HEAD_SHA: git('rev-parse', 'HEAD').trim(), + DEVELOPMENT_BRANCH: 'dev', + TARGET_REF: 'refs/heads/issue/7-own-work', + }, + }); + assert.equal(r.status, 0, r.stdout + r.stderr); + const report = JSON.parse(r.stdout); + assert.equal(report.commits, 1, JSON.stringify(report)); + assert.equal(report.ok, true, JSON.stringify(report.findings)); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + test('an infrastructure range is recognised by the absence of class A files (#207)', () => { const infra = makeCommit({ sha: 'a'.repeat(40), subject: 'Tune CI', body: 'Issue: #206\nUser-Visible: no',