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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user