mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 16:46:31 +03:00
Close the integration-branch guard bypass; accept local upstreams
Cursor Bugbot round two, both real: an upstream or origin/HEAD naming a
DIFFERENT integration branch bypassed the conventional-name guard, so
sitting on develop with the remote default at main still produced the
integration-vs-integration divergence this detection exists to prevent.
And splitRemoteRef returned null for a slashless @{u}, silently dropping
local upstreams (branch.<x>.remote = ".").
Base detection is now skipped entirely on an integration branch: no
signal may override the working-tree scope there. A slashless upstream
resolves as its own name and rev. Two failing-first tests: origin/HEAD
pointing at main while sitting on develop, and a feature branch
tracking a local canary branch.
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
ea098ceb96
commit
b9d294b29c
@@ -104,31 +104,40 @@ function gitSignals(cwd) {
|
||||
const i = ref ? ref.indexOf('/') : -1;
|
||||
return i > 0 ? { name: ref.slice(i + 1), rev: ref } : null;
|
||||
};
|
||||
const upstream = splitRemoteRef(run(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']));
|
||||
const remoteHead = splitRemoteRef(run(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']));
|
||||
// A slashless @{u} is a LOCAL upstream (branch.<x>.remote = "."); it names
|
||||
// a merge target just as validly as a remote-tracking ref does.
|
||||
const asUpstream = (ref) => splitRemoteRef(ref) || (ref ? { name: ref, rev: ref } : null);
|
||||
const conventional = ['develop', 'main', 'master'];
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
const addCandidate = (name, revs) => {
|
||||
if (!name || name === branch || seen.has(name)) return;
|
||||
seen.add(name);
|
||||
candidates.push({ name, revs });
|
||||
};
|
||||
// The upstream tracks the actual merge target, so its remote rev wins over
|
||||
// a possibly stale local branch of the same name.
|
||||
if (upstream) addCandidate(upstream.name, [upstream.rev]);
|
||||
if (remoteHead) addCandidate(remoteHead.name, [remoteHead.name, remoteHead.rev]);
|
||||
if (!conventional.includes(branch)) {
|
||||
for (const name of conventional) addCandidate(name, [name, `origin/${name}`]);
|
||||
}
|
||||
// On an integration branch itself the scope hint is the working tree. No
|
||||
// signal may override that: an origin/HEAD or upstream naming a DIFFERENT
|
||||
// integration branch (sitting on develop while the remote default is
|
||||
// main) would produce exactly the integration-vs-integration divergence
|
||||
// this detection exists to prevent.
|
||||
const onIntegrationBranch = conventional.includes(branch);
|
||||
let base = null;
|
||||
let baseRev = null;
|
||||
for (const c of candidates) {
|
||||
const rev = c.revs.find((r) => run(['rev-parse', '--verify', '--quiet', r]) !== null);
|
||||
if (rev) {
|
||||
base = c.name;
|
||||
baseRev = rev;
|
||||
break;
|
||||
if (!onIntegrationBranch) {
|
||||
const upstream = asUpstream(run(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']));
|
||||
const remoteHead = splitRemoteRef(run(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']));
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
const addCandidate = (name, revs) => {
|
||||
if (!name || name === branch || seen.has(name)) return;
|
||||
seen.add(name);
|
||||
candidates.push({ name, revs });
|
||||
};
|
||||
// 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]);
|
||||
if (remoteHead) addCandidate(remoteHead.name, [remoteHead.name, remoteHead.rev]);
|
||||
for (const name of conventional) 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) {
|
||||
base = c.name;
|
||||
baseRev = rev;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const diffBase = base && branch && branch !== base ? base : null;
|
||||
|
||||
@@ -309,6 +309,48 @@ describe('gatherSignals', () => {
|
||||
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' });
|
||||
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');
|
||||
git('checkout', '-q', 'develop');
|
||||
// The remote default is main; sitting on develop must still not produce
|
||||
// a develop-vs-main integration diff via the origin/HEAD signal.
|
||||
git('update-ref', 'refs/remotes/origin/main', 'main');
|
||||
git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
|
||||
write('src/App.tsx', 'export default 2;\n'); // dirty on develop
|
||||
const s = await gatherSignals(scratch);
|
||||
assert.equal(s.git.base, null);
|
||||
assert.deepEqual(s.git.changedFiles, ['src/App.tsx']);
|
||||
});
|
||||
|
||||
it('honors a local (slashless) upstream branch (#302)', async () => {
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
|
||||
git('init', '-q', '-b', 'canary');
|
||||
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('checkout', '-q', '-b', 'feature/u');
|
||||
git('branch', '-q', '--set-upstream-to=canary'); // local upstream, no remote
|
||||
write('src/Hero.tsx', 'export const Hero = () => null;\n');
|
||||
git('add', '.');
|
||||
git('commit', '-qm', 'feature work');
|
||||
const s = await gatherSignals(scratch);
|
||||
// canary is neither conventional nor remote, but the configured
|
||||
// upstream names it as the merge target.
|
||||
assert.equal(s.git.base, 'canary');
|
||||
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