- 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:
- Have an open PR whose head branch lives in the same repository, e.g.
feat/my-feature.
- 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.
- Switch to some other branch.
- 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.
Steps to Reproduce:
feat/my-feature.origin/feat/my-feature, or you push from another machine or worktree. Local is nowbehind 1, ahead 0.Expected: the existing local
feat/my-featureis 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-featureis left behind at its stale commit, and the PR is now associated with thepr/branch instead.Cause
fetchAndCheckoutdecides whether to create thepr/branch by comparing SHAs with!==:vscode-pull-request-github/src/github/pullRequestGitHelper.ts
Lines 113 to 124 in 849821a
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:
vscode-pull-request-github/src/github/pullRequestGitHelper.ts
Lines 143 to 147 in 849821a
branch.behind > 0 && branch.ahead === 0is 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,branchandlocalBranchNamehave been reassigned to the freshly createdpr/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: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
associateBranchWithPullRequestwritesbranch.pr/<author>/<number>.github-pr-owner-numberinto.git/config. Subsequent checkouts of the same PR then takecheckoutExistingPullRequestBranch, which finds that association and returns to thepr/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/configby hand.