fix(git): handle worktrees from forked PRs safely (#2693)

* fix(git): create worktrees from forked PRs via refs/pull/<n>/head fallback

A worktree created from a linked GitHub PR whose head branch lives in a fork
failed when the fork's head repository was missing (deleted fork) or
unfetchable (auth, network): the dialog threw 'PR head repository URL is
unavailable' before any git command ran, and the server had no fallback to
refs/pull/<n>/head, which GitHub serves on the base repository.

- NewWorktreeDialog: when pr.headRepo is absent, send a prRef config
  (refs/pull/<n>/head from origin) instead of throwing; the fork config now
  also carries prRef so the server can fall back when the fork fetch fails.
- git service: fetchPullRequestHeadRef fetches refs/pull/<n>/head into
  refs/remotes/<remote>/pr-<n>-head (same refspec shape as
  fetchRemoteBranchRef) and both validateWorktreeCreate and
  attachGitWorktreeToCandidate fall back to it when the fork path fails;
  fallback worktrees get --no-track and no upstream config because a PR ref
  is not pushable. When both paths fail the original fork error surfaces.
- Focused tests cover the prRef-only path and the fork-unreachable fallback.

Fixes #2422

* fix(git): harden PR worktree fallback against stale fork refs (#12)

After a fork fetch fails, resolve immediately from refs/pull/<n>/head
instead of accepting a cached remotes/<fork>/<branch> tracking ref.
Match the PR base repository by URL (not a hardcoded origin remote),
store fetched PR heads under refs/openchamber/pull/<n>/head, and share
one existing-mode resolver between validate and create.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* fix(git): make PR head SHA authoritative and namespace private refs

Reuse local/remote branches for linked PRs only when their tip matches
pr.headSha; otherwise fall through to fork fetch / refs/pull. Store PR
heads under refs/openchamber/github/<owner>/<repo>/pull/<n>/head, prefer
HTTPS for direct base-repo fallback, and surface composite fork+fallback
errors when both paths fail.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* test(ui): assert validate/create forward deleted-fork PR payload fields

Guards the dialog wiring regression where validate omitted prRef while
create included it, by asserting worktreeManager forwards prRef,
prBaseRepoUrl, and related fields for deleted-fork configs.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor(git): always checkout linked PRs from refs/pull/<n>/head

Move PR worktree resolution to the server. The UI now sends only
pullRequest identity (number + baseRepoUrl + optional head fields);
the server always fetches the authoritative PR head and best-effort
configures fork upstream afterward.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor(ui): drop PrWorktreeConfig; send PR identity only

Delete the prWorktreeConfig module. NewWorktreeDialog maps linked PRs
straight to pullRequest identity, skips upstream defaults for that path,
and leaves checkout + optional fork tracking to the server.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor(git): linked PRs are {number, baseRepoUrl} only

Drop fork upstream / tracking and head/base owner-repo fields from the
linked-PR worktree path. Fetch refs/pull/<n>/head, create --no-track, done.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* fix(git): meet #2422 Must/Should without refs/pull fallback

Linked PRs send fork identity only; the server provisions pr-<owner>,
fetches the head branch, and fails clearly when the fork is missing or
unreachable. Local reuse requires a matching headSha. Prefer HTTPS for
headRepoUrl. Do not write upstream tracking when the upstream ref was
never fetched.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* fix(git): drop invalid upstream fallback and PR branch collisions

Remove setBranchTrackingFallback: if upstream fetch fails, leave tracking
unset. When a linked PR's head branch already exists locally with a
different tip, create pr-<number> instead of git worktree add -b on the
colliding name.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* fix(git): strip PR worktree create back to fork-remote provision (#15)

Keep the original ensureRemoteName/Url path for linked fork PRs, prefer
HTTPS clone URLs, fail clearly when the fork is unreachable, and leave
upstream tracking unset when the upstream ref was never fetched.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>


Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Serhii Dziupin
2026-08-18 16:56:55 +03:00
committed by GitHub
co-authored by Serhii Dziupin
parent bf0dfc4e6b
commit 9832c0a4a8
5 changed files with 345 additions and 85 deletions
@@ -52,6 +52,14 @@ The following functions are exported and used by the web server:
- `removeWorktree(directory, input)`: Remove a worktree (optionally delete local branch).
- `isLinkedWorktree(directory)`: Check if directory is a linked worktree (not primary).
### Worktree creation from a GitHub pull request
The UI provisions `pr-<owner>` via `ensureRemoteName`/`ensureRemoteUrl`
(HTTPS clone URL preferred over SSH) and checks out
`remotes/pr-<owner>/<head>`. A missing head URL or unreachable fork fails with
a clear error before a worktree is kept. If upstream fetch fails during
bootstrap, tracking is left unset rather than writing `branch.*.remote` /
`branch.*.merge` for a ref that was never fetched.
### Commit and Remote Operations
- `commit(directory, message, options)`: Create a commit from the current index. `options.stageFiles` may be provided with `options.files` by older callers to stage only selected unstaged rows before committing, but the shared Git panel now stages/unstages explicitly before commit.
- `pull(directory, options)`: Pull changes from remote.