An existing develop outranks a main-pointing origin/HEAD

Cursor Bugbot's remaining round-1 finding held for the current code
too: in a git-flow repo whose platform default was never flipped off
main, a feature branch without an upstream picked origin/HEAD's main
over the develop branch features actually merge to, dragging the
develop-vs-main divergence into scan targets. develop now sits between
the upstream signal and origin/HEAD in the candidate order; repos
without a develop branch are unaffected. Failing-first test covers the
exact shape (develop exists, origin/HEAD -> main).

Prepared with AI assistance (Claude Code), directed by @pbakaus.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-25 20:17:55 -07:00
co-authored by Claude Code
parent b9d294b29c
commit e82653965c
2 changed files with 31 additions and 1 deletions
+6 -1
View File
@@ -129,8 +129,13 @@ function gitSignals(cwd) {
// The upstream tracks the actual merge target, so its own rev wins over
// a possibly stale local branch of the same name.
if (upstream) addCandidate(upstream.name, [upstream.rev]);
// A develop branch marks a git-flow repo where features merge to develop
// 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']);
if (remoteHead) addCandidate(remoteHead.name, [remoteHead.name, remoteHead.rev]);
for (const name of conventional) addCandidate(name, [name, `origin/${name}`]);
for (const name of ['main', 'master']) addCandidate(name, [name, `origin/${name}`]);
for (const c of candidates) {
const rev = c.revs.find((r) => run(['rev-parse', '--verify', '--quiet', r]) !== null);
if (rev) {
+25
View File
@@ -309,6 +309,31 @@ describe('gatherSignals', () => {
assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
});
it('an existing develop outranks a main-pointing origin/HEAD (#302)', async () => {
const { execFileSync } = await import('node:child_process');
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
git('init', '-q', '-b', 'main');
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('branch', '-q', 'develop');
// Classic git-flow with the platform default never flipped off main.
git('update-ref', 'refs/remotes/origin/main', 'main');
git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
git('checkout', '-q', 'develop');
git('checkout', '-q', '-b', 'feature/g');
write('src/Hero.tsx', 'export const Hero = () => null;\n');
git('add', '.');
git('commit', '-qm', 'feature work');
const s = await gatherSignals(scratch);
// Features merge to develop here; picking origin/HEAD's main would drag
// the develop-vs-main divergence into scan targets.
assert.equal(s.git.base, 'develop');
assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
});
it('remote signals cannot bypass the integration-branch guard (#302)', async () => {
const { execFileSync } = await import('node:child_process');
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });