mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 16:46:31 +03:00
Cover every remote in each candidate's rev list
Cursor and Greptile converged on one root cause from the previous round: candidate revs stopped at origin (develop tried only develop and origin/develop; a remote-default entry carried only its own rev), so the name-level dedup discarded a same-name base living on another remote. A fork-parent layout with develop only as upstream/develop, or a pruned origin/main beside a live upstream/main, lost its base entirely. revsFor(name) now expands to the local branch plus <remote>/<name> for every remote (origin first), and all named candidates use it, which is exactly what makes the dedup safe. Two failing-first tests cover the upstream-only develop and the pruned-origin/live-upstream main shapes. Prepared with AI assistance (Claude Code), directed by @pbakaus. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Code
parent
46f29ca8b3
commit
386d3e7051
@@ -131,6 +131,13 @@ function gitSignals(cwd) {
|
||||
let baseRev = null;
|
||||
if (!onIntegrationBranch) {
|
||||
const upstream = asUpstream(run(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']));
|
||||
// Every named candidate tries the local branch first, then that name on
|
||||
// every remote (origin first). Covering all remotes up front is what
|
||||
// makes the name-level dedup below safe: a develop or main that exists
|
||||
// only as upstream/<name> still resolves even though origin's candidate
|
||||
// claimed the name first.
|
||||
const remoteOrder = ['origin', ...remotes.filter((name) => name !== 'origin')];
|
||||
const revsFor = (name) => [name, ...remoteOrder.map((r) => `${r}/${name}`)];
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
const addCandidate = (name, revs) => {
|
||||
@@ -145,9 +152,9 @@ function gitSignals(cwd) {
|
||||
// even when the platform default (origin/HEAD) was never flipped off
|
||||
// main; an existing develop therefore outranks the remote default. This
|
||||
// is #302's own repro shape, and repos without develop are unaffected.
|
||||
addCandidate('develop', ['develop', 'origin/develop']);
|
||||
for (const head of remoteHeads) addCandidate(head.name, [head.name, head.rev]);
|
||||
for (const name of ['main', 'master']) addCandidate(name, [name, `origin/${name}`]);
|
||||
addCandidate('develop', revsFor('develop'));
|
||||
for (const head of remoteHeads) addCandidate(head.name, revsFor(head.name));
|
||||
for (const name of ['main', 'master']) addCandidate(name, revsFor(name));
|
||||
for (const c of candidates) {
|
||||
const rev = c.revs.find((r) => run(['rev-parse', '--verify', '--quiet', r]) !== null);
|
||||
if (rev) {
|
||||
|
||||
@@ -436,6 +436,52 @@ describe('gatherSignals', () => {
|
||||
assert.deepEqual(s.git.changedFiles, ['src/App.tsx']);
|
||||
});
|
||||
|
||||
it('finds a develop that exists only on a non-origin remote (#302)', async () => {
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
|
||||
git('init', '-q', '-b', 'feature/f');
|
||||
git('config', 'user.email', 't@example.com');
|
||||
git('config', 'user.name', 'Test');
|
||||
write('src/App.tsx', 'export default 1;\n');
|
||||
git('add', '.');
|
||||
git('commit', '-qm', 'init');
|
||||
// Fork-parent layout: develop lives only as upstream/develop, no local
|
||||
// copy, no origin remote, and the feature branch has no upstream.
|
||||
git('remote', 'add', 'upstream', '.');
|
||||
git('update-ref', 'refs/remotes/upstream/develop', 'HEAD');
|
||||
write('src/Hero.tsx', 'export const Hero = () => null;\n');
|
||||
git('add', '.');
|
||||
git('commit', '-qm', 'feature work');
|
||||
const s = await gatherSignals(scratch);
|
||||
assert.equal(s.git.base, 'develop');
|
||||
assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
|
||||
});
|
||||
|
||||
it('a same-name default on a second remote still resolves (#302)', async () => {
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
|
||||
git('init', '-q', '-b', 'feature/h');
|
||||
git('config', 'user.email', 't@example.com');
|
||||
git('config', 'user.name', 'Test');
|
||||
write('src/App.tsx', 'export default 1;\n');
|
||||
git('add', '.');
|
||||
git('commit', '-qm', 'init');
|
||||
git('remote', 'add', 'origin', '.');
|
||||
git('remote', 'add', 'upstream', '.');
|
||||
// origin advertises main but its tracking ref is gone (pruned); the
|
||||
// real main lives only as upstream/main. Name-level dedup must not
|
||||
// discard the upstream rev.
|
||||
git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
|
||||
git('update-ref', 'refs/remotes/upstream/main', 'HEAD');
|
||||
git('symbolic-ref', 'refs/remotes/upstream/HEAD', 'refs/remotes/upstream/main');
|
||||
write('src/Hero.tsx', 'export const Hero = () => null;\n');
|
||||
git('add', '.');
|
||||
git('commit', '-qm', 'feature work');
|
||||
const s = await gatherSignals(scratch);
|
||||
assert.equal(s.git.base, 'main');
|
||||
assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
|
||||
});
|
||||
|
||||
it('never diffs one integration branch against another (#302)', async () => {
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
|
||||
|
||||
Reference in New Issue
Block a user