Show the current pull request after switching branches (#2699)

* fix(git): reconcile pull requests after branch changes

Treat persisted change-request metadata as a branch-bound hint and prefer current tracking configuration when it identifies the checked-out branch.

* fix(git): preserve enterprise fork identity

Use branch-bound metadata to supplement the owner when explicit tracking points at the same head on a self-hosted remote.

* fix(git): honor local branch upstreams

Treat Git remote dot with a merge ref as explicit current tracking so stale PR metadata cannot override a local upstream.

* fix(git): own change request branch bindings

Centralize persisted hint validation and rebinding so renames preserve change request identity while switches invalidate it, including normalized legacy Enterprise owners.

* fix(git): preserve PR identity through workspace renames

Resolve the backing worktree root during rename without adding a Git call, and retain fork owner identity from explicit same-name tracking configuration.

* fix(git): trust current enterprise fork identity

Explicit tracking now derives repository identity from the current remote path, so stale checkout metadata cannot override a repointed fork.

* fix(git): recognize enterprise repository aliases

GitHub repository identity is case-insensitive. Normalize equality checks so differently cased aliases cannot turn base tracking into a fork lookup.

* fix(git): reject ownerless legacy fork matches

Owner-qualified legacy hints only belong to their historical owner-prefixed branch names. A later ownerless branch reusing the same head must resolve from current Git state instead.

* fix(git): keep fork feature lookups local

A feature branch may track the upstream base branch for ancestry even when origin is a fork. Only treat that cross-repository base ref as PR identity when the local branch name identifies the tracked head.
This commit is contained in:
Mohamed Boudra
2026-07-31 23:06:10 +08:00
committed by GitHub
parent baa1c08ec8
commit 1ddca07d57
6 changed files with 484 additions and 24 deletions
+62 -4
View File
@@ -6,6 +6,7 @@ const ChangeRequestLookupTargetSchema = z.object({
headRef: z.string().min(1),
headRepositoryOwner: z.string().min(1).optional(),
changeRequestNumber: z.number().int().positive().optional(),
localBranchName: z.string().min(1).optional(),
});
const PaseoWorktreeMetadataV1Schema = z.object({
@@ -44,9 +45,66 @@ const PaseoWorktreeMetadataSchema = z.union([
]);
export type PaseoWorktreeMetadata = z.infer<typeof PaseoWorktreeMetadataSchema>;
export type PaseoWorktreeChangeRequestLookupTarget = z.infer<
typeof ChangeRequestLookupTargetSchema
>;
export type PaseoWorktreeChangeRequestHint = z.infer<typeof ChangeRequestLookupTargetSchema>;
export function createPaseoWorktreeChangeRequestHint(
input: PaseoWorktreeChangeRequestHint,
): PaseoWorktreeChangeRequestHint {
return ChangeRequestLookupTargetSchema.parse(input);
}
export function getPaseoWorktreeChangeRequestHintForBranch(
metadata: PaseoWorktreeMetadata | null,
currentBranch: string,
): PaseoWorktreeChangeRequestHint | null {
const target = metadata?.changeRequestLookupTarget;
if (!target) {
return null;
}
if (target.localBranchName) {
return target.localBranchName === currentBranch ? target : null;
}
// COMPAT(change-request-local-branch): metadata before v0.2.5 omitted the
// local binding; remove after 2027-07-31.
const canonicalBranches = new Set<string>();
if (target.headRepositoryOwner) {
canonicalBranches.add(`${target.headRepositoryOwner}/${target.headRef}`);
const normalizedOwner = normalizeLegacyGitHubOwnerForBranch(target.headRepositoryOwner);
if (normalizedOwner) {
canonicalBranches.add(`${normalizedOwner}/${target.headRef}`);
}
} else {
canonicalBranches.add(target.headRef);
}
return canonicalBranches.has(currentBranch) ? target : null;
}
function normalizeLegacyGitHubOwnerForBranch(owner: string): string | null {
const normalized = owner.trim().toLowerCase();
return /^[a-z0-9-]+$/.test(normalized) ? normalized : null;
}
export function rebindPaseoWorktreeChangeRequestHint(
worktreeRoot: string,
previousBranch: string,
currentBranch: string,
): boolean {
const metadata = readPaseoWorktreeMetadata(worktreeRoot);
const target = getPaseoWorktreeChangeRequestHintForBranch(metadata, previousBranch);
if (!metadata || !target) {
return false;
}
writePaseoWorktreeMetadataFile(worktreeRoot, {
...metadata,
changeRequestLookupTarget: {
...target,
localBranchName: currentBranch,
},
});
return true;
}
function getGitDirForWorktreeRoot(worktreeRoot: string): string {
const gitPath = join(worktreeRoot, ".git");
@@ -90,7 +148,7 @@ export function writePaseoWorktreeMetadata(
worktreeRoot: string,
options: {
baseRefName: string;
changeRequestLookupTarget?: PaseoWorktreeChangeRequestLookupTarget;
changeRequestLookupTarget?: PaseoWorktreeChangeRequestHint;
},
): void {
const baseRefName = normalizeBaseRefName(options.baseRefName);