[Bug]: Fork PR creation always fails — head selector is computed against `origin` instead of gh's resolved base repo
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
Fork-based workflow, where gh's resolved base repo is not origin:
- Clone a fork so
origin=<you>/<repo>andupstream=<org>/<repo>. - Let
ghresolve the base repo toupstream(gh repo set-default <org>/<repo>, or answer its prompt once). This writesremote.upstream.gh-resolved=base. - Create a branch, commit, and push it to
originso it tracksorigin/<branch>. The branch exists only on the fork. - Use T3 Code's source-control action to create a PR for that branch.
Expected behavior
A cross-repository PR is created from <you>:<branch> into <org>/<repo>, matching what bare gh pr create does in the same worktree.
Actual behavior
Source control provider github failed in createChangeRequest: GitHub CLI command failed.
gh exits 1 because it is asked for a head branch that does not exist in the base repo.
Cause
resolveBranchHeadContext in apps/server/src/git/GitManager.ts decides cross-repository-ness by comparing the branch's remote against the literal remote named origin:
const [remoteRepository, originRepository] = yield* Effect.all([
resolveRemoteRepositoryContext(cwd, remoteName),
resolveRemoteRepositoryContext(cwd, "origin"), // <-- assumes origin is the base repo
], { concurrency: "unbounded" });
const isCrossRepository =
remoteRepository.repositoryNameWithOwner !== null && originRepository.repositoryNameWithOwner !== null
? remoteRepository.repositoryNameWithOwner.toLowerCase() !==
originRepository.repositoryNameWithOwner.toLowerCase()
: remoteName !== null && remoteName !== "origin" && remoteRepository.repositoryNameWithOwner !== null;
// ...
preferredHeadSelector: ownerHeadSelector && isCrossRepository ? ownerHeadSelector : headBranch,
In a fork clone the branch tracks origin, and origin is the fork — so remoteRepository === originRepository, isCrossRepository is false, and the owner-qualified ownerHeadSelector (<you>:<branch>) is discarded in favour of the bare headBranch.
That bare value flows to GitHubCli.createPullRequest (apps/server/src/sourceControl/GitHubCli.ts), which passes it as an explicit --head:
args: ["pr", "create", "--base", input.baseBranch, "--head", input.headSelector,
"--title", input.title, "--body-file", input.bodyFile]
An explicit --head suppresses gh's own tracking-branch inference, so gh looks for <branch> in the base repo it resolved (upstream), does not find it, and exits non-zero.
The asymmetry is the core of it: the base branch is resolved through gh — resolveBaseBranch falls through to getDefaultBranch, i.e. gh repo view --json defaultBranchRef, which honours gh-resolved and therefore reports upstream's default branch — while the head selector is computed against origin. Base comes from one repo, head is assumed to live in another.
Searching the shipped 0.0.33 server bundle for gh-resolved, set-default, or any base-repo resolution returns nothing, so this appears to be a general assumption rather than a slip: whenever gh's base repo is not origin, cross-repository PR creation sends an unqualified head.
Evidence
$ gh repo set-default --view
<org>/<repo>
$ git config --get-regexp gh-resolved
remote.upstream.gh-resolved base
$ git rev-parse --abbrev-ref @{u}
origin/<branch>
# the head T3 Code sends is absent from the base repo
$ gh api repos/<org>/<repo>/branches/<branch>
gh: Branch not found (HTTP 404)
# ...but it is a valid cross-fork head
$ gh api "repos/<org>/<repo>/compare/main...<you>:<branch>" --jq .status
ahead
Same command shape, with and without the explicit --head T3 Code passes:
$ gh pr create --base main --title t --body-file /dev/null --dry-run
head: <you>:<branch> # gh infers the owner prefix
$ gh pr create --base main --head <branch> --title t --body-file /dev/null --dry-run
head: <branch> # what T3 Code sends -> 404 against the base repo
Suggested fix
Compare the head remote against the provider's resolved base repository rather than the remote literally named origin — read remote.<name>.gh-resolved / gh repo view --json nameWithOwner and use that as the comparison target. ownerHeadSelector is already computed correctly; only the predicate that gates it is wrong.
Relationship to existing issues
- #3801 (closed, completed) fixed this same
originassumption, but on the read path (selectProviderContext,baseRefChoices.ts,GitActionsControl.tsx).GitManager.resolveBranchHeadContextis on the write path and was not in that scope, so PR creation still assumesorigin. - #3694 reports the identical user-facing string from a different cause (Windows, single-repo, dry run succeeds). Not a duplicate — but its point stands here too: because
GitHubCliCommandError.detailis the constant"GitHub CLI command failed."andgh's stderr never reaches the error or the logs, this bug is invisible from the app. The underlyingghmessage names the missing head branch directly.
Impact
Major degradation or frequent failure
Version or commit
T3 Code v0.0.33 (Linux AppImage)
Environment
- OS: Linux (Ubuntu, KDE Plasma 6, Wayland)
- GitHub CLI:
gh version 2.97.0 - Remotes:
origin= fork (HTTPS),upstream= source repo (HTTPS) ghbase repo:upstream, viaremote.upstream.gh-resolved=base- Branch tracks
origin/<branch>; the branch exists only on the fork
Workaround
Push the branch to whichever repo gh resolves as base, so the bare head resolves there:
git push -u upstream <branch>
git config remote.pushDefault upstream # keeps future branches out of the trap
Repointing the base repo at the fork (gh repo set-default <you>/<repo>) also makes T3 Code succeed, but then PRs target the fork instead of upstream, which is usually not what you want. There is no local configuration that makes T3 Code open a genuine fork -> upstream PR.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in apps/server/src/git/GitManager.ts at resolveBranchHeadContext and trace how the resolved base repository and head remote are selected. Check apps/server/src/sourceControl/GitHubCli.ts to confirm the resulting head selector passed to gh pr create. Reproduce the fork/upstream workflow, then verify that a cross-repository PR uses the owner-qualified head and succeeds without requiring the branch in the base repository.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, github, typescript
- Domain
- backend, devtools
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100