Fix pushed state for checked-out PR worktrees (#1804)

* fix(worktree): track checkout PR branches correctly

Checkout-PR worktrees could point branch tracking at a PR push remote without fetching a matching remote-tracking ref. Configure same-repo PRs to track origin, make fork PR remotes fetchable before assigning upstream, and push fork PR branches to their configured remote.

* fix(worktree): scope PR push routing to Paseo targets

Review caught that using ordinary upstream config for Push could send normal branches to non-origin remotes and mishandle deduped PR branch names. Store a Paseo-specific push target during PR worktree creation, then have Push honor only that marker before falling back to origin.

* fix(worktree): use git upstream for PR pushes

* fix(worktree): allow PR checkout without head upstream

* fix(worktree): preserve fork PR git push target

* fix(worktree): preserve PR push targets

* fix(worktree): preserve PR push status

* fix(worktree): set upstream after push-only PR push
This commit is contained in:
Mohamed Boudra
2026-06-29 20:21:19 +02:00
committed by GitHub
parent 31d14ec8a7
commit f31bc2b8b7
6 changed files with 998 additions and 29 deletions
+120 -11
View File
@@ -166,6 +166,7 @@ export type WorktreeSource =
baseRefName: string;
localBranchName?: string;
pushRemoteUrl?: string;
trackOriginHead?: boolean;
};
export interface CreateWorktreeOptions {
@@ -1201,6 +1202,13 @@ export const createWorktree = async ({
remote: sourcePlan.pushRemote,
});
}
if (sourcePlan.trackingRemote) {
await configureWorktreeTrackingRemote({
cwd,
branchName: sourcePlan.branchName,
remote: sourcePlan.trackingRemote,
});
}
writePaseoWorktreeMetadata(worktreePath, { baseRefName: sourcePlan.metadataBaseRefName });
@@ -1245,6 +1253,11 @@ interface WorktreeSourcePlan {
name: string;
url: string;
headRef: string;
track: boolean;
};
trackingRemote?: {
name: string;
headRef: string;
};
}
@@ -1309,20 +1322,42 @@ async function resolveWorktreeSourcePlan({
timeout: 120_000,
},
);
const trackingRemote = source.trackOriginHead
? await tryFetchWorktreeTrackingRemote({
cwd,
remoteName: "origin",
headRef: source.headRef,
})
: undefined;
const remotePlan: Pick<WorktreeSourcePlan, "pushRemote" | "trackingRemote"> = {};
if (source.pushRemoteUrl) {
const remoteName = `paseo-pr-${source.githubPrNumber}`;
remotePlan.pushRemote = {
name: remoteName,
url: source.pushRemoteUrl,
headRef: source.headRef,
track: true,
};
} else if (source.trackOriginHead && localBranchName !== source.headRef) {
const originUrl = await getWorktreeRemotePushUrl(cwd, "origin");
if (originUrl) {
remotePlan.pushRemote = {
name: `paseo-pr-${source.githubPrNumber}`,
url: originUrl,
headRef: source.headRef,
track: false,
};
}
}
if (trackingRemote) {
remotePlan.trackingRemote = trackingRemote;
}
return {
branchName: localBranchName,
metadataBaseRefName: normalizedBaseRefName,
addArguments: [localBranchName],
...(source.pushRemoteUrl
? {
pushRemote: {
name: `paseo-pr-${source.githubPrNumber}`,
url: source.pushRemoteUrl,
headRef: source.headRef,
},
}
: {}),
...remotePlan,
};
}
}
@@ -1335,6 +1370,7 @@ async function configureWorktreePushRemote(options: {
name: string;
url: string;
headRef: string;
track: boolean;
};
}): Promise<void> {
await runGitCommand(["config", `remote.${options.remote.name}.url`, options.remote.url], {
@@ -1344,11 +1380,84 @@ async function configureWorktreePushRemote(options: {
["config", `remote.${options.remote.name}.push`, `HEAD:refs/heads/${options.remote.headRef}`],
{ cwd: options.cwd },
);
await runGitCommand(["config", `branch.${options.branchName}.remote`, options.remote.name], {
await runGitCommand(["config", `branch.${options.branchName}.pushRemote`, options.remote.name], {
cwd: options.cwd,
});
if (!options.remote.track) {
return;
}
await runGitCommand(
["config", `branch.${options.branchName}.merge`, `refs/heads/${options.remote.headRef}`],
[
"config",
`remote.${options.remote.name}.fetch`,
`+refs/heads/${options.remote.headRef}:refs/remotes/${options.remote.name}/${options.remote.headRef}`,
],
{ cwd: options.cwd },
);
const trackingRemote = await tryFetchWorktreeTrackingRemote({
cwd: options.cwd,
remoteName: options.remote.name,
headRef: options.remote.headRef,
});
if (trackingRemote) {
await configureWorktreeTrackingRemote({
cwd: options.cwd,
branchName: options.branchName,
remote: trackingRemote,
});
}
}
async function tryFetchWorktreeTrackingRemote(options: {
cwd: string;
remoteName: string;
headRef: string;
}): Promise<{ name: string; headRef: string } | undefined> {
const result = await runGitCommand(
[
"fetch",
options.remoteName,
`+refs/heads/${options.headRef}:refs/remotes/${options.remoteName}/${options.headRef}`,
],
{
cwd: options.cwd,
timeout: 120_000,
acceptExitCodes: [0, 1, 128],
},
);
return result.exitCode === 0 ? { name: options.remoteName, headRef: options.headRef } : undefined;
}
async function getWorktreeRemotePushUrl(
cwd: string,
remoteName: string,
): Promise<string | undefined> {
try {
const { stdout } = await runGitCommand(["remote", "get-url", "--push", remoteName], {
cwd,
});
const url = stdout.trim();
return url.length > 0 ? url : undefined;
} catch {
return undefined;
}
}
async function configureWorktreeTrackingRemote(options: {
cwd: string;
branchName: string;
remote: {
name: string;
headRef: string;
};
}): Promise<void> {
await runGitCommand(
[
"branch",
"--set-upstream-to",
`${options.remote.name}/${options.remote.headRef}`,
options.branchName,
],
{ cwd: options.cwd },
);
}