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:
Bohdan Triapitsyn
2026-08-04 22:41:09 +03:00
parent 9aa8a3e375
commit ce0e1cea27
9 changed files with 292 additions and 37 deletions
+21 -2
View File
@@ -1417,12 +1417,32 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
}));
}, [remotes, remoteBranches, remoteUrl, status?.tracking]);
const currentBranch = status?.current ?? null;
// The repository's own default branch, so a repo whose default is neither
// main, master nor develop stops being compared against a branch that does
// not exist.
const defaultBranch = React.useMemo(() => {
const trackingRemote = status?.tracking?.trim().split('/')[0];
return (trackingRemote && branches?.defaultBranches?.[trackingRemote])
?? branches?.defaultBranches?.origin;
}, [branches, status?.tracking]);
const baseBranch = React.useMemo(() => deriveBaseBranch({
remoteNames: new Set(effectiveRemotes.map((remote) => remote.name)),
localBranches,
worktreeCreatedFromBranch: worktreeMetadata?.createdFromBranch,
rootBranchHint,
}), [effectiveRemotes, localBranches, rootBranchHint, worktreeMetadata?.createdFromBranch]);
defaultBranch,
headBranch: currentBranch,
}), [
currentBranch,
defaultBranch,
effectiveRemotes,
localBranches,
rootBranchHint,
worktreeMetadata?.createdFromBranch,
]);
const updateTargetBranch = React.useMemo(() => {
const remoteNames = effectiveRemotes.map((remote) => remote.name);
@@ -1511,7 +1531,6 @@ export const GitView: React.FC<GitViewProps> = ({ isActive }) => {
const stagedCount = stagedChangeEntries.length;
const isBusy = isLoading || syncAction !== null || commitAction !== null;
const currentBranch = status?.current ?? null;
const canShowIntegrateCommitsSection = Boolean(
worktreeMetadata && repoRootForIntegrate && sourceBranchForIntegrate && shouldShowIntegrateCommits
);