Accept remote refs as diff bases; honor non-origin upstreams

Both review bots found real gaps in the first pass: candidates were
verified as local branch names only, so an origin/HEAD target with no
local checkout fell through, and stripOrigin() dropped upstreams on any
remote not named origin (fork workflows tracking upstream/release).

Candidates now carry a display name plus the revs to try in order: the
upstream's remote rev wins outright (it tracks the actual merge target,
so it beats a possibly stale local branch of the same name), origin/HEAD
tries the local branch then the remote-tracking ref, and the
conventional names each try local then origin/<name>. git.base keeps
reporting the friendly branch name while the diff runs against whichever
rev resolved. Two new failing-first tests: remote-only default branch,
and an upstream on a remote named upstream with no local base branch.

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 a50702f2b6
commit ea098ceb96
2 changed files with 74 additions and 12 deletions
+43
View File
@@ -266,6 +266,49 @@ describe('gatherSignals', () => {
assert.deepEqual(s.git.changedFiles, ['src/App.tsx']);
});
it('uses the remote-tracking ref when the base has no local branch (#302)', async () => {
const { execFileSync } = await import('node:child_process');
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
git('init', '-q', '-b', 'develop');
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('update-ref', 'refs/remotes/origin/develop', 'HEAD');
git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/develop');
git('checkout', '-q', '-b', 'feature/w');
git('branch', '-q', '-D', 'develop'); // remote default exists, local doesn't
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('honors an upstream on a non-origin remote (fork workflow) (#302)', async () => {
const { execFileSync } = await import('node:child_process');
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
git('init', '-q', '-b', 'release');
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', 'upstream', '.');
git('update-ref', 'refs/remotes/upstream/release', 'HEAD');
git('checkout', '-q', '-b', 'feature/v');
git('branch', '-q', '--set-upstream-to=upstream/release');
git('branch', '-q', '-D', 'release'); // the tracked base lives only on the fork parent
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, 'release');
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' });