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:
@@ -11,6 +11,7 @@ import {
|
||||
createWorktree,
|
||||
getWorktreeBootstrapStatus,
|
||||
getBranches,
|
||||
getRangeDiff,
|
||||
getStatus,
|
||||
isGitRepository,
|
||||
populateWorktreeWithLockRecovery,
|
||||
@@ -48,6 +49,28 @@ const runGit = (cwd, args) =>
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
/**
|
||||
* A repository on `next` whose only remote publishes `defaultBranch` and has it
|
||||
* recorded as that remote's HEAD — the shape of every repository whose default
|
||||
* branch is not one of the conventional names.
|
||||
*/
|
||||
const createRepositoryWithRemote = ({ remoteName = 'origin', defaultBranch = 'react' } = {}) => {
|
||||
const remote = createTempDir();
|
||||
const repository = createTempDir();
|
||||
runGit(remote, ['init', '--bare', `--initial-branch=${defaultBranch}`]);
|
||||
runGit(repository, ['init', '-b', 'next']);
|
||||
runGit(repository, ['config', 'user.email', 'test@example.com']);
|
||||
runGit(repository, ['config', 'user.name', 'Test']);
|
||||
fs.writeFileSync(path.join(repository, 'README.md'), '# Test\n');
|
||||
runGit(repository, ['add', 'README.md']);
|
||||
runGit(repository, ['commit', '-m', 'init']);
|
||||
runGit(repository, ['remote', 'add', remoteName, remote]);
|
||||
runGit(repository, ['push', remoteName, `HEAD:${defaultBranch}`]);
|
||||
runGit(repository, ['fetch', remoteName]);
|
||||
runGit(repository, ['remote', 'set-head', remoteName, '--auto']);
|
||||
return { remote, repository };
|
||||
};
|
||||
|
||||
const canRunGit = () => {
|
||||
try {
|
||||
execFileSync('git', ['--version'], { stdio: 'ignore' });
|
||||
@@ -992,22 +1015,47 @@ describe('hash validation', () => {
|
||||
|
||||
describe.runIf(canRunGit())('getBranches', () => {
|
||||
it('returns a remote default branch whose name is not a conventional fallback', async () => {
|
||||
const remote = createTempDir();
|
||||
const repository = createTempDir();
|
||||
runGit(remote, ['init', '--bare', '--initial-branch=react']);
|
||||
runGit(repository, ['init', '-b', 'next']);
|
||||
runGit(repository, ['config', 'user.email', 'test@example.com']);
|
||||
runGit(repository, ['config', 'user.name', 'Test']);
|
||||
fs.writeFileSync(path.join(repository, 'README.md'), '# Test\n');
|
||||
runGit(repository, ['add', 'README.md']);
|
||||
runGit(repository, ['commit', '-m', 'init']);
|
||||
runGit(repository, ['remote', 'add', 'origin', remote]);
|
||||
runGit(repository, ['push', 'origin', 'HEAD:react']);
|
||||
runGit(repository, ['fetch', 'origin']);
|
||||
runGit(repository, ['remote', 'set-head', 'origin', '--auto']);
|
||||
const { repository } = createRepositoryWithRemote({ remoteName: 'origin', defaultBranch: 'react' });
|
||||
|
||||
await expect(getBranches(repository)).resolves.toMatchObject({
|
||||
defaultBranches: { origin: 'react' },
|
||||
});
|
||||
});
|
||||
|
||||
it('asks the remote when no local remote/HEAD exists', async () => {
|
||||
const { repository } = createRepositoryWithRemote({ remoteName: 'origin', defaultBranch: 'react' });
|
||||
// A hand-added remote can end up without this ref; the branch it points at
|
||||
// is still knowable, and guessing instead is the bug this data replaces.
|
||||
runGit(repository, ['remote', 'set-head', 'origin', '--delete']);
|
||||
|
||||
await expect(getBranches(repository)).resolves.toMatchObject({
|
||||
defaultBranches: { origin: 'react' },
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the branches of a remote that cannot be reached', async () => {
|
||||
const { repository, remote } = createRepositoryWithRemote({ remoteName: 'origin', defaultBranch: 'react' });
|
||||
fs.rmSync(remote, { recursive: true, force: true });
|
||||
|
||||
const branches = await getBranches(repository);
|
||||
|
||||
// "We could not ask" is not "the branch is gone": callers read this list to
|
||||
// decide whether a base branch exists at all.
|
||||
expect(branches.all).toContain('remotes/origin/react');
|
||||
});
|
||||
});
|
||||
|
||||
describe.runIf(canRunGit())('getRangeDiff', () => {
|
||||
it('resolves a base that exists only on a remote other than origin', async () => {
|
||||
const { repository } = createRepositoryWithRemote({ remoteName: 'upstream', defaultBranch: 'react' });
|
||||
// Only refs/remotes/upstream/react carries the base — git cannot resolve the
|
||||
// bare name, so an unqualified `react...next` fails with "ambiguous argument".
|
||||
fs.writeFileSync(path.join(repository, 'feature.txt'), 'work\n');
|
||||
runGit(repository, ['add', 'feature.txt']);
|
||||
runGit(repository, ['commit', '-m', 'feature']);
|
||||
|
||||
const diff = await getRangeDiff(repository, { base: 'react', head: 'next' });
|
||||
|
||||
expect(diff).toContain('feature.txt');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user