fix(git): resolve the base branch from the repository instead of its name
Follow-up to #2629, which stopped the walkthrough from comparing against a branch that does not exist. The same guessing, and the same near-misses in how the answer was applied, were left elsewhere: - The default branch travelled as `rootBranchHint`, whose documented meaning is "the branch the project root worktree is on". It gets its own option, because a parameter that means two things is one the next caller gets wrong. - A candidate equal to the branch being compared is skipped. In a plain checkout the root hint *is* the current branch, so it won every time and produced a comparison with itself; the repository default now wins there. - The Changes and pull-request surfaces read the default branch too. A pull request opened against a branch that does not exist is a worse failure than a walkthrough that will not generate. - `hasResolvableBaseBranch` matched `origin/feature/main` for a base of `main`, passing the check and then failing the comparison it exists to prevent. - `getRangeDiff` promoted only `origin/<base>`. A base carried by any other remote stayed a bare name, which git resolves against refs/heads and nowhere else, so it failed exactly as before. - `getBranches` dropped every branch of a remote that did not answer, turning "we could not ask" into "these branches are gone" — offline, that silently removed comparisons that work fine against local remote-tracking refs. - A remote with no `remote/HEAD` is asked once with `ls-remote --symref` rather than falling back to the guess this data exists to replace. The `defaultBranches` contract was documented under the status response; it belongs to the branches response, which now has a section of its own.
This commit is contained in:
@@ -2446,6 +2446,27 @@ export async function getRangeDiff(directory, { base, head, path: filePath, cont
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Not every repository has an `origin`. When the base names a branch that
|
||||
// exists only on another remote, a bare name does not resolve — git looks in
|
||||
// refs/heads, not across remotes — and the diff fails with "ambiguous
|
||||
// argument". Fall back to whichever remote actually carries it.
|
||||
if (resolvedBase === baseRef && !/[*?[\]^~:\\]/.test(baseRef)) {
|
||||
const resolvesLocally = await git
|
||||
.raw(['rev-parse', '--verify', `refs/heads/${baseRef}`])
|
||||
.then((value) => Boolean(String(value || '').trim()))
|
||||
.catch(() => false);
|
||||
|
||||
if (!resolvesLocally) {
|
||||
const remoteMatch = await git
|
||||
.raw(['for-each-ref', '--count=1', '--format=%(refname:short)', `refs/remotes/*/${baseRef}`])
|
||||
.then((value) => String(value || '').trim())
|
||||
.catch(() => '');
|
||||
if (remoteMatch) {
|
||||
resolvedBase = remoteMatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const args = ['diff', '--no-color'];
|
||||
if (typeof contextLines === 'number' && !Number.isNaN(contextLines)) {
|
||||
args.push(`-U${Math.max(0, contextLines)}`);
|
||||
@@ -3387,13 +3408,15 @@ export async function getBranches(directory) {
|
||||
}
|
||||
|
||||
async function getRemoteDefaultBranches(git) {
|
||||
let defaults = {};
|
||||
|
||||
try {
|
||||
const refs = await git.raw([
|
||||
'for-each-ref',
|
||||
'--format=%(refname) %(symref)',
|
||||
'refs/remotes',
|
||||
]);
|
||||
return Object.fromEntries(
|
||||
defaults = Object.fromEntries(
|
||||
refs.trim().split('\n').flatMap((line) => {
|
||||
const [ref, symbolicRef] = line.split(' ');
|
||||
const match = ref.match(/^refs\/remotes\/([^/]+)\/HEAD$/);
|
||||
@@ -3404,8 +3427,38 @@ async function getRemoteDefaultBranches(git) {
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
return {};
|
||||
defaults = {};
|
||||
}
|
||||
|
||||
// `remote/HEAD` is written by clone and by `git remote set-head`; a remote
|
||||
// added by hand may never have one. Without this the caller falls back to
|
||||
// guessing main/master/develop, which is exactly the guess this data exists
|
||||
// to replace — so ask the remote itself, but only for the remotes that are
|
||||
// actually missing an answer.
|
||||
try {
|
||||
const remotes = await git.getRemotes();
|
||||
const missing = remotes.filter((remote) => remote?.name && !defaults[remote.name]);
|
||||
if (missing.length === 0) return defaults;
|
||||
|
||||
const resolved = await Promise.all(missing.map(async (remote) => {
|
||||
try {
|
||||
const output = await git.raw(['ls-remote', '--symref', remote.name, 'HEAD']);
|
||||
const match = String(output || '').match(/^ref:\s+refs\/heads\/(.+?)\s+HEAD$/m);
|
||||
return match ? [remote.name, match[1]] : null;
|
||||
} catch {
|
||||
// Unreachable or refusing: no answer is better than a guessed one.
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
for (const entry of resolved) {
|
||||
if (entry) defaults[entry[0]] = entry[1];
|
||||
}
|
||||
} catch {
|
||||
// Remote list unavailable; the local symrefs are still valid.
|
||||
}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
async function filterActiveRemoteBranches(git, remoteBranches) {
|
||||
@@ -3413,6 +3466,13 @@ async function filterActiveRemoteBranches(git, remoteBranches) {
|
||||
const remotes = await git.getRemotes();
|
||||
const branchesByRemote = new Map();
|
||||
|
||||
// A remote that did not answer says nothing about its branches. Dropping
|
||||
// them would turn "we could not ask" into "these branches are gone", and
|
||||
// callers use this list to decide whether a base branch exists at all — so
|
||||
// offline would silently remove comparisons that work perfectly well
|
||||
// against the local remote-tracking refs.
|
||||
const unreachableRemotes = new Set();
|
||||
|
||||
await Promise.all(remotes.map(async (remote) => {
|
||||
try {
|
||||
const lsRemoteResult = await git.raw(['ls-remote', '--heads', remote.name]);
|
||||
@@ -3426,7 +3486,7 @@ async function filterActiveRemoteBranches(git, remoteBranches) {
|
||||
}
|
||||
branchesByRemote.set(remote.name, actualRemoteBranches);
|
||||
} catch {
|
||||
// Skip remotes that fail (e.g., unreachable)
|
||||
unreachableRemotes.add(remote.name);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -3435,6 +3495,7 @@ async function filterActiveRemoteBranches(git, remoteBranches) {
|
||||
if (!match) return false;
|
||||
const remoteName = remoteBranch.split('/')[1];
|
||||
const branchName = match[1];
|
||||
if (unreachableRemotes.has(remoteName)) return true;
|
||||
return branchesByRemote.get(remoteName)?.has(branchName) ?? false;
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user