pingdotgg / pingdotgg/t3code

[Bug]: Fork PR creation always fails — head selector is computed against `origin` instead of gh's resolved base repo

Open
#7,382 1 comment 0 reactions 0 assignees View on GitHub

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:

  1. Clone a fork so origin = <you>/<repo> and upstream = <org>/<repo>.
  2. Let gh resolve the base repo to upstream (gh repo set-default <org>/<repo>, or answer its prompt once). This writes remote.upstream.gh-resolved=base.
  3. Create a branch, commit, and push it to origin so it tracks origin/<branch>. The branch exists only on the fork.
  4. 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 ghresolveBaseBranch 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 origin assumption, but on the read path (selectProviderContext, baseRefChoices.ts, GitActionsControl.tsx). GitManager.resolveBranchHeadContext is on the write path and was not in that scope, so PR creation still assumes origin.
  • #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.detail is the constant "GitHub CLI command failed." and gh's stderr never reaches the error or the logs, this bug is invisible from the app. The underlying gh message 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)
  • gh base repo: upstream, via remote.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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.