Skip to content

"Checkout Pull Request" creates a pr/<author>/<number> branch when the local branch is only behind the remote #8900

Description

@ffxsam
  • Extension version: 0.162.0
  • VSCode Version: 1.134.0
  • OS: macOS 26.5.2 (arm64)
  • Repository Clone Configuration: single repository (not a fork)
  • GitHub Product: GitHub.com

Steps to Reproduce:

  1. Have an open PR whose head branch lives in the same repository, e.g. feat/my-feature.
  2. Have that branch checked out locally, then let it fall behind the remote — a teammate pushes to origin/feat/my-feature, or you push from another machine or worktree. Local is now behind 1, ahead 0.
  3. Switch to some other branch.
  4. Invoke Checkout Pull Request on that PR.

Expected: the existing local feat/my-feature is checked out and fast-forwarded to the remote commit.

Actual: a new branch pr/<author>/<number> is created at the remote commit and checked out. feat/my-feature is left behind at its stale commit, and the PR is now associated with the pr/ branch instead.

Cause

fetchAndCheckout decides whether to create the pr/ branch by comparing SHAs with !==:

// Check if local branch is pointing to the same commit as the remote
if (branch.commit !== trackedBranch.commit) {
Logger.appendLine(`Local branch ${localBranchName} commit ${branch.commit} differs from remote commit ${trackedBranch.commit}. Creating new branch to avoid overwriting user's work.`, PullRequestGitHelper.ID);
// Instead of deleting the user's branch, create a unique branch name to avoid conflicts
const uniqueBranchName = await PullRequestGitHelper.calculateUniqueBranchNameForPR(repository, pullRequest);
Logger.appendLine(`Creating branch ${uniqueBranchName} for PR checkout`, PullRequestGitHelper.ID);
progress.report({ message: vscode.l10n.t('Creating branch {0} for pull request', uniqueBranchName) });
await repository.createBranch(uniqueBranchName, false, trackedBranch.commit);
await repository.setBranchUpstream(uniqueBranchName, trackedBranchName);
// Use the unique branch name for checkout
localBranchName = uniqueBranchName;
branch = await repository.getBranch(localBranchName);

A branch that is purely behind trips that condition, but there is no user work to overwrite — which is what both the log line and the adjacent comment give as the reason for creating the branch.

Thirty lines further down, the same function already computes the distinction it needs:

if (branch.behind !== undefined && branch.behind > 0 && branch.ahead === 0) {
Logger.debug(`Pull from upstream`, PullRequestGitHelper.ID);
progress.report({ message: vscode.l10n.t('Pulling {0}', localBranchName) });
await repository.pull();
}

branch.behind > 0 && branch.ahead === 0 is exactly "safe to fast-forward". So the function does distinguish "has local commits" from "just stale" — the rename decision simply doesn't consult it, and by the time that check runs, branch and localBranchName have been reassigned to the freshly created pr/ branch, so the pull is a no-op.

This looks like an over-correction of #7702, which was about an unrelated local branch that happened to share a name with the PR head. Comparing SHAs does fix that case, but it also catches the ordinary case where the local branch really is the PR branch and is merely out of date — which is far more common.

Suggested fix

Only diverge to a pr/ branch when the local branch actually carries commits the remote doesn't have, or isn't tracking the PR's head at all — roughly:

const isStaleButSafe = branch.ahead === 0 && branch.upstream?.name === originalBranchName;
if (branch.commit !== trackedBranch.commit && !isStaleButSafe) {
    // ... existing unique-branch path
}

and otherwise fall through to the existing fast-forward path below. That keeps #7702 fixed (an unrelated same-named branch won't be tracking the PR's head) without renaming branches that only need a pull.

Side effect: the rename is sticky

associateBranchWithPullRequest writes branch.pr/<author>/<number>.github-pr-owner-number into .git/config. Subsequent checkouts of the same PR then take checkoutExistingPullRequestBranch, which finds that association and returns to the pr/ branch — correctly pulling it, but never picking the original branch back up. Once a repo has been through this, the original branch name is only recoverable by editing .git/config by hand.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions