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:
committed by
GitHub
co-authored by
Serhii Dziupin
parent
bf0dfc4e6b
commit
9832c0a4a8
@@ -98,20 +98,6 @@ const normalizeBranchName = (value: string): string => {
|
||||
.replace(/^\/+|\/+$/g, '');
|
||||
};
|
||||
|
||||
const slugifyWorktreeName = (value: string): string => {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^refs\/heads\//, '')
|
||||
.replace(/^heads\//, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/^\/+|\/+$/g, '')
|
||||
.split('/').join('-')
|
||||
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 80);
|
||||
};
|
||||
|
||||
const sanitizeRemoteName = (value: string): string => {
|
||||
const normalized = String(value || '')
|
||||
.trim()
|
||||
@@ -165,10 +151,14 @@ const resolvePrWorktreeConfig = (pr: GitHubPullRequestSummary, localBranches: st
|
||||
const ownerFromLabel = String(pr.headLabel || '').split(':')[0]?.trim();
|
||||
const remoteSeed = pr.headRepo?.owner || ownerFromLabel || 'pr-head';
|
||||
const remoteName = `pr-${sanitizeRemoteName(remoteSeed)}`;
|
||||
const remoteUrl = pr.headRepo?.sshUrl || pr.headRepo?.cloneUrl || '';
|
||||
// Prefer HTTPS so anonymous public fetches do not require SSH agent setup.
|
||||
const remoteUrl = pr.headRepo?.cloneUrl || pr.headRepo?.sshUrl || '';
|
||||
|
||||
if (!remoteUrl) {
|
||||
throw new Error('PR head repository URL is unavailable');
|
||||
throw new Error(
|
||||
'PR head repository URL is unavailable. The fork may have been deleted; '
|
||||
+ 'push the branch to a reachable repository and try again.'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -182,6 +172,20 @@ const resolvePrWorktreeConfig = (pr: GitHubPullRequestSummary, localBranches: st
|
||||
};
|
||||
};
|
||||
|
||||
const slugifyWorktreeName = (value: string): string => {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^refs\/heads\//, '')
|
||||
.replace(/^heads\//, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/^\/+|\/+$/g, '')
|
||||
.split('/').join('-')
|
||||
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 80);
|
||||
};
|
||||
|
||||
interface NewWorktreeDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -898,7 +902,7 @@ export function NewWorktreeDialog({
|
||||
...(sourceBranch && mode === 'new-branch' ? { startRef: sourceBranch } : {}),
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
const resolvedArgs = await withWorktreeUpstreamDefaults(projectDirectory, args);
|
||||
|
||||
const metadata = await createWorktree(projectRef, resolvedArgs);
|
||||
|
||||
Reference in New Issue
Block a user