mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
refactor(forge): one head ref kind on the checkout target
hasOriginHeadBranch and hasHeadBranch could describe contradictory states (cross-repo checkout claiming an origin head branch). collapse both into headRefKind: "branch" | "pull-ref" and derive origin tracking in the resolver instead of storing it separately. Co-authored-by: Alexander Adam <alexander.adam@vade.io>
This commit is contained in:
committed by
paseo-bot[bot]
parent
1be237d353
commit
1947acf276
@@ -187,7 +187,7 @@ describe("resolveWorktreeCreationIntent", () => {
|
||||
headRepositorySshUrl: null,
|
||||
headRepositoryUrl: null,
|
||||
isCrossRepository: false,
|
||||
hasOriginHeadBranch: false,
|
||||
headRefKind: "pull-ref",
|
||||
});
|
||||
|
||||
const intent = await resolveWorktreeCreationIntent(
|
||||
@@ -351,8 +351,7 @@ describe("resolveWorktreeCreationIntent", () => {
|
||||
headRepositorySshUrl: null,
|
||||
headRepositoryUrl: null,
|
||||
isCrossRepository: false,
|
||||
hasOriginHeadBranch: false,
|
||||
hasHeadBranch: false,
|
||||
headRefKind: "pull-ref",
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -386,7 +385,7 @@ describe("resolveWorktreeCreationIntent", () => {
|
||||
headRepositorySshUrl: "git@gitea.test:contributor/repo.git",
|
||||
headRepositoryUrl: "https://gitea.test/contributor/repo",
|
||||
isCrossRepository: true,
|
||||
hasHeadBranch: false,
|
||||
headRefKind: "pull-ref",
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -419,7 +418,7 @@ describe("resolveWorktreeCreationIntent", () => {
|
||||
headRepositorySshUrl: null,
|
||||
headRepositoryUrl: null,
|
||||
isCrossRepository: true,
|
||||
hasHeadBranch: false,
|
||||
headRefKind: "pull-ref",
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -192,7 +192,7 @@ async function resolvePrCheckoutIntent(
|
||||
const localBranchName = service.buildPrLocalBranchName?.({ headRef, checkoutTarget });
|
||||
const crossRepository = resolveCrossRepositoryFields(checkoutTarget);
|
||||
const trackOriginHead =
|
||||
!checkoutTarget.isCrossRepository && checkoutTarget.hasOriginHeadBranch !== false;
|
||||
!checkoutTarget.isCrossRepository && checkoutTarget.headRefKind !== "pull-ref";
|
||||
|
||||
return {
|
||||
kind: "checkout-change-request",
|
||||
|
||||
@@ -39,10 +39,8 @@ export interface PullRequestCheckoutTarget {
|
||||
headRepositorySshUrl: string | null;
|
||||
headRepositoryUrl: string | null;
|
||||
isCrossRepository: boolean;
|
||||
/** false when origin has no head branch to track */
|
||||
hasOriginHeadBranch?: boolean;
|
||||
/** false when headRefName is a synthetic pull ref, not a real branch name (agit PRs, deleted branches) */
|
||||
hasHeadBranch?: boolean;
|
||||
/** "pull-ref" when headRefName is a synthetic pull ref, not a real branch name (agit PRs, deleted branches); undefined means "branch" */
|
||||
headRefKind?: "branch" | "pull-ref";
|
||||
}
|
||||
|
||||
export interface PullRequestCheckoutRef {
|
||||
|
||||
@@ -2303,8 +2303,7 @@ describe("createGiteaService", () => {
|
||||
headRepositorySshUrl: null,
|
||||
headRepositoryUrl: null,
|
||||
isCrossRepository: false,
|
||||
hasOriginHeadBranch: true,
|
||||
hasHeadBranch: true,
|
||||
headRefKind: "branch",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2341,8 +2340,7 @@ describe("createGiteaService", () => {
|
||||
headRepositorySshUrl: "git@gitea.com:contributor/sample-repo.git",
|
||||
headRepositoryUrl: "https://gitea.com/contributor/sample-repo",
|
||||
isCrossRepository: true,
|
||||
hasOriginHeadBranch: false,
|
||||
hasHeadBranch: true,
|
||||
headRefKind: "branch",
|
||||
});
|
||||
|
||||
expect(calls).toContainEqual(["api", "repos/example-user/sample-repo/pulls/5"]);
|
||||
@@ -2369,8 +2367,7 @@ describe("createGiteaService", () => {
|
||||
{ remoteName: "upstream", remoteRef: "refs/pull/5/head" },
|
||||
],
|
||||
isCrossRepository: false,
|
||||
hasOriginHeadBranch: false,
|
||||
hasHeadBranch: false,
|
||||
headRefKind: "pull-ref",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2418,8 +2415,7 @@ describe("createGiteaService", () => {
|
||||
{ remoteName: "upstream", remoteRef: "refs/pull/5/head" },
|
||||
],
|
||||
isCrossRepository: false,
|
||||
hasOriginHeadBranch: false,
|
||||
hasHeadBranch: false,
|
||||
headRefKind: "pull-ref",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2439,8 +2435,7 @@ describe("createGiteaService", () => {
|
||||
],
|
||||
headOwnerLogin: null,
|
||||
isCrossRepository: true,
|
||||
hasOriginHeadBranch: false,
|
||||
hasHeadBranch: true,
|
||||
headRefKind: "branch",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2457,7 +2452,7 @@ describe("createGiteaService", () => {
|
||||
remoteName: "origin",
|
||||
remoteRef: "refs/heads/feat/sample-change",
|
||||
});
|
||||
expect(target.hasOriginHeadBranch).toBe(true);
|
||||
expect(target.headRefKind).toBe("branch");
|
||||
});
|
||||
|
||||
it("prefixes the local branch name with the fork owner for a cross-repository checkout", () => {
|
||||
@@ -2492,7 +2487,7 @@ describe("createGiteaService", () => {
|
||||
headRepositorySshUrl: null,
|
||||
headRepositoryUrl: null,
|
||||
isCrossRepository: false,
|
||||
hasHeadBranch: false,
|
||||
headRefKind: "pull-ref",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2512,7 +2507,7 @@ describe("createGiteaService", () => {
|
||||
headRepositorySshUrl: null,
|
||||
headRepositoryUrl: null,
|
||||
isCrossRepository: true,
|
||||
hasHeadBranch: false,
|
||||
headRefKind: "pull-ref",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2532,7 +2527,7 @@ describe("createGiteaService", () => {
|
||||
headRepositorySshUrl: null,
|
||||
headRepositoryUrl: null,
|
||||
isCrossRepository: true,
|
||||
hasHeadBranch: false,
|
||||
headRefKind: "pull-ref",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1445,19 +1445,30 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For
|
||||
return runJsonParse(args, runOptions, stdout, schema);
|
||||
}
|
||||
|
||||
function resolveHeadRefKind(
|
||||
pr: z.infer<typeof GiteaPullRequestApiSchema>,
|
||||
number: number,
|
||||
): "branch" | "pull-ref" {
|
||||
// head.ref only becomes the branch name when the branch still exists,
|
||||
// agit PRs and deleted branches keep the pull ref
|
||||
const isPullRefOnly =
|
||||
pr.flow === GITEA_PULL_REQUEST_FLOW_AGIT || pr.head?.ref === `refs/pull/${number}/head`;
|
||||
return isPullRefOnly ? "pull-ref" : "branch";
|
||||
}
|
||||
|
||||
async function resolveForkCheckoutFacts(
|
||||
cwd: string,
|
||||
summary: PullRequestSummary,
|
||||
): Promise<{
|
||||
isCrossRepository: boolean;
|
||||
hasHeadBranch: boolean;
|
||||
headRefKind: "branch" | "pull-ref";
|
||||
headOwnerLogin: string | null;
|
||||
headRepositorySshUrl: string | null;
|
||||
headRepositoryUrl: string | null;
|
||||
}> {
|
||||
const sameRepo = {
|
||||
isCrossRepository: false,
|
||||
hasHeadBranch: true,
|
||||
headRefKind: "branch" as const,
|
||||
headOwnerLogin: null,
|
||||
headRepositorySshUrl: null,
|
||||
headRepositoryUrl: null,
|
||||
@@ -1476,18 +1487,14 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For
|
||||
);
|
||||
const headRepo = pr.head?.repo ?? null;
|
||||
const baseRepo = pr.base?.repo ?? null;
|
||||
// head.ref only becomes the branch name when the branch still exists,
|
||||
// agit PRs and deleted branches keep the pull ref
|
||||
const hasHeadBranch =
|
||||
pr.flow !== GITEA_PULL_REQUEST_FLOW_AGIT &&
|
||||
pr.head?.ref !== `refs/pull/${summary.number}/head`;
|
||||
const headRefKind = resolveHeadRefKind(pr, summary.number);
|
||||
if (baseRepo?.id == null || headRepo?.id === baseRepo.id) {
|
||||
return { ...sameRepo, hasHeadBranch };
|
||||
return { ...sameRepo, headRefKind };
|
||||
}
|
||||
// headRepo is null when the fork was deleted
|
||||
return {
|
||||
isCrossRepository: true,
|
||||
hasHeadBranch,
|
||||
headRefKind,
|
||||
headOwnerLogin: headRepo?.owner?.login ?? null,
|
||||
headRepositorySshUrl: headRepo?.ssh_url ?? null,
|
||||
headRepositoryUrl: headRepo?.html_url ?? null,
|
||||
@@ -2033,7 +2040,7 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For
|
||||
const checkoutRefs = [originPullRef];
|
||||
// for a fork PR a branch with that name on origin belongs to the base repo
|
||||
const headBranchOnOrigin =
|
||||
!fork.isCrossRepository && fork.hasHeadBranch && Boolean(summary.headRefName);
|
||||
!fork.isCrossRepository && fork.headRefKind === "branch" && Boolean(summary.headRefName);
|
||||
if (headBranchOnOrigin) {
|
||||
checkoutRefs.push({ remoteName: "origin", remoteRef: `refs/heads/${summary.headRefName}` });
|
||||
}
|
||||
@@ -2048,8 +2055,7 @@ export function createGiteaService(options: CreateGiteaServiceOptions = {}): For
|
||||
headRepositorySshUrl: fork.headRepositorySshUrl,
|
||||
headRepositoryUrl: fork.headRepositoryUrl,
|
||||
isCrossRepository: fork.isCrossRepository,
|
||||
hasOriginHeadBranch: headBranchOnOrigin,
|
||||
hasHeadBranch: fork.hasHeadBranch,
|
||||
headRefKind: fork.headRefKind,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -797,6 +797,7 @@ describe("ForgeService", () => {
|
||||
headRepositorySshUrl: "git@github.com:therainisme/paseo.git",
|
||||
headRepositoryUrl: "https://github.com/therainisme/paseo",
|
||||
isCrossRepository: true,
|
||||
headRefKind: "branch",
|
||||
});
|
||||
|
||||
expect(runner.calls).toHaveLength(2);
|
||||
|
||||
@@ -3522,6 +3522,7 @@ function toPullRequestCheckoutTarget(
|
||||
headRepositorySshUrl: pullRequest.headRepository?.sshUrl || null,
|
||||
headRepositoryUrl: pullRequest.headRepository?.url || null,
|
||||
isCrossRepository: pullRequest.isCrossRepository,
|
||||
headRefKind: "branch",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -477,6 +477,7 @@ describe("createGitLabService", () => {
|
||||
headRepositorySshUrl: null,
|
||||
headRepositoryUrl: null,
|
||||
isCrossRepository: false,
|
||||
headRefKind: "branch",
|
||||
});
|
||||
expect(calls[0]).toEqual(["mr", "view", "14", "-F", "json"]);
|
||||
});
|
||||
|
||||
@@ -1204,6 +1204,7 @@ export function createGitLabService(options: CreateGitLabServiceOptions = {}): F
|
||||
mr.target_project_id !== undefined &&
|
||||
mr.target_project_id !== null &&
|
||||
mr.source_project_id !== mr.target_project_id,
|
||||
headRefKind: "branch",
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -70,20 +70,20 @@ describe("buildForkLocalBranchName", () => {
|
||||
...fork,
|
||||
isCrossRepository: false,
|
||||
headOwnerLogin: "arthur",
|
||||
hasHeadBranch: false,
|
||||
headRefKind: "pull-ref",
|
||||
}),
|
||||
).toBe("pr-42");
|
||||
});
|
||||
|
||||
it("prefixes pr-<number> with the owner for a fork pull-ref-only head", () => {
|
||||
expect(
|
||||
buildForkLocalBranchName({ ...fork, headOwnerLogin: "Arthur", hasHeadBranch: false }),
|
||||
buildForkLocalBranchName({ ...fork, headOwnerLogin: "Arthur", headRefKind: "pull-ref" }),
|
||||
).toBe("arthur/pr-42");
|
||||
});
|
||||
|
||||
it("does not double the pr number when the fork owner is unknown and there is no head branch", () => {
|
||||
expect(buildForkLocalBranchName({ ...fork, headOwnerLogin: null, hasHeadBranch: false })).toBe(
|
||||
"pr-42",
|
||||
);
|
||||
expect(
|
||||
buildForkLocalBranchName({ ...fork, headOwnerLogin: null, headRefKind: "pull-ref" }),
|
||||
).toBe("pr-42");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,15 +26,15 @@ export interface ForkLocalBranchNameInput {
|
||||
number: number;
|
||||
isCrossRepository: boolean;
|
||||
headOwnerLogin: string | null;
|
||||
/** false when headRef is a synthetic pull ref, not a real branch name (agit PRs, deleted branches) */
|
||||
hasHeadBranch?: boolean;
|
||||
/** "pull-ref" when headRef is a synthetic pull ref, not a real branch name (agit PRs, deleted branches); undefined means "branch" */
|
||||
headRefKind?: "branch" | "pull-ref";
|
||||
}
|
||||
|
||||
export function buildForkLocalBranchName(params: ForkLocalBranchNameInput): string {
|
||||
const pullRefFallback = `pr-${params.number}`;
|
||||
// headRef is the literal pull ref (e.g. refs/pull/42/head) when there is no
|
||||
// real branch to name after, so use the pr number instead
|
||||
const headSegment = params.hasHeadBranch === false ? pullRefFallback : params.headRef;
|
||||
const headSegment = params.headRefKind === "pull-ref" ? pullRefFallback : params.headRef;
|
||||
if (!params.isCrossRepository) {
|
||||
return headSegment;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user