Fix commit-aware PR resolution across forges

This commit is contained in:
nllptrx
2026-07-18 14:21:10 +02:00
committed by GitHub
parent ffe76a7e57
commit c97f823236
16 changed files with 879 additions and 122 deletions
+87 -9
View File
@@ -48,7 +48,11 @@ import { startGitCommandMetrics, stopGitCommandMetrics } from "./run-git-command
import { createForgeResolver } from "../services/forge-resolver.js";
import { GitHubCommandError, GitHubCliMissingError } from "../services/github-service.js";
import type { CurrentPullRequestStatus, ForgeService } from "../services/forge-service.js";
import { TeaAuthenticationError, TeaCliMissingError } from "../services/gitea-service.js";
import {
TeaAuthenticationError,
TeaCliMissingError,
TeaCommandError,
} from "../services/gitea-service.js";
import {
createWorktree as createWorktreePrimitive,
type CreateWorktreeOptions,
@@ -161,6 +165,7 @@ function createPullRequestStatus(overrides?: Partial<CurrentPullRequestStatus>)
interface RequestedPullRequestTarget {
headRef: string;
headSha?: string;
headRepositoryOwner?: string;
}
@@ -176,6 +181,7 @@ function createGitHubServiceRecordingPullRequestTargets(
github.getCurrentPullRequestStatus = async (request) => {
options.requestedTargets.push({
headRef: request.headRef,
...(request.headSha ? { headSha: request.headSha } : {}),
...(request.headRepositoryOwner ? { headRepositoryOwner: request.headRepositoryOwner } : {}),
});
return createPullRequestStatus({
@@ -2341,7 +2347,8 @@ const x = 1;
const lookupTarget = await readPullRequestLookupTargetFromFacts(repoDir, paseoHome);
expect(lookupTarget).toEqual({ headRef: "refactor/workspace-scripts" });
expect(lookupTarget).toMatchObject({ headRef: "refactor/workspace-scripts" });
expect(lookupTarget?.headSha).toMatch(/^[0-9a-f]{40}$/);
});
it("keeps the local branch lookup when origin tracking uses the same head name", async () => {
@@ -2356,7 +2363,8 @@ const x = 1;
const lookupTarget = await readPullRequestLookupTargetFromFacts(repoDir, paseoHome);
expect(lookupTarget).toEqual({ headRef: "feature" });
expect(lookupTarget).toMatchObject({ headRef: "feature" });
expect(lookupTarget?.headSha).toMatch(/^[0-9a-f]{40}$/);
});
it("does not attach an owner when the tracked remote is the same GitHub repository", async () => {
@@ -2378,7 +2386,8 @@ const x = 1;
const lookupTarget = await readPullRequestLookupTargetFromFacts(repoDir, paseoHome);
expect(lookupTarget).toEqual({ headRef: "refactor/workspace-scripts" });
expect(lookupTarget).toMatchObject({ headRef: "refactor/workspace-scripts" });
expect(lookupTarget?.headSha).toMatch(/^[0-9a-f]{40}$/);
});
it("keeps the fork owner when same-repo comparison is indeterminate", async () => {
@@ -2396,7 +2405,8 @@ const x = 1;
const lookupTarget = await readPullRequestLookupTargetFromFacts(repoDir, paseoHome);
expect(lookupTarget).toEqual({ headRef: "main", headRepositoryOwner: "chethanuk" });
expect(lookupTarget).toMatchObject({ headRef: "main", headRepositoryOwner: "chethanuk" });
expect(lookupTarget?.headSha).toMatch(/^[0-9a-f]{40}$/);
});
it("uses the configured push remote for fork PR lookup when upstream is absent", async () => {
@@ -2425,8 +2435,11 @@ const x = 1;
);
expect(getBranchUpstream(repoDir)).toBeNull();
expect(factsTarget).toEqual({ headRef: "main", headRepositoryOwner: "chethanuk" });
expect(requestedTargets).toEqual([{ headRef: "main", headRepositoryOwner: "chethanuk" }]);
expect(factsTarget).toMatchObject({ headRef: "main", headRepositoryOwner: "chethanuk" });
expect(factsTarget?.headSha).toMatch(/^[0-9a-f]{40}$/);
expect(requestedTargets).toEqual([
expect.objectContaining({ headRef: "main", headRepositoryOwner: "chethanuk" }),
]);
});
it("keeps the local branch lookup when same-repo tracking points at the base branch", async () => {
@@ -2441,7 +2454,8 @@ const x = 1;
const lookupTarget = await readPullRequestLookupTargetFromFacts(repoDir, paseoHome);
expect(lookupTarget).toEqual({ headRef: "tender-parrot" });
expect(lookupTarget).toMatchObject({ headRef: "tender-parrot" });
expect(lookupTarget?.headSha).toMatch(/^[0-9a-f]{40}$/);
});
it("derives the same origin tracked head for on-demand PR status reads", async () => {
@@ -2495,7 +2509,9 @@ const x = 1;
const status = await getPullRequestStatus(repoDir, github);
expect(requestedTargets).toEqual([{ headRef: "main", headRepositoryOwner: "chethanuk" }]);
expect(requestedTargets).toEqual([
expect.objectContaining({ headRef: "main", headRepositoryOwner: "chethanuk" }),
]);
expect(status.status?.number).toBe(345);
expect(status.status?.headRefName).toBe("main");
});
@@ -2544,6 +2560,33 @@ const x = 1;
expect(callCount).toBe(1);
});
it("does not reuse a PR status cache entry after HEAD changes on the same branch", async () => {
execFileSync("git", ["checkout", "-b", "feature"], { cwd: repoDir });
execFileSync("git", ["remote", "add", "origin", "https://github.com/getpaseo/paseo.git"], {
cwd: repoDir,
});
const requestedShas: string[] = [];
const github = createGitHubServiceForStatus(null);
github.getCurrentPullRequestStatus = async (options) => {
if (options.headSha) requestedShas.push(options.headSha);
return createPullRequestStatus({
url: `https://github.com/getpaseo/paseo/pull/${requestedShas.length}`,
});
};
const first = await getPullRequestStatus(repoDir, github);
writeFileSync(join(repoDir, "next.txt"), "next\n");
execFileSync("git", ["add", "next.txt"], { cwd: repoDir });
execFileSync("git", ["commit", "-m", "next commit"], { cwd: repoDir });
const second = await getPullRequestStatus(repoDir, github);
expect(first.status?.url).toContain("/pull/1");
expect(second.status?.url).toContain("/pull/2");
expect(requestedShas).toHaveLength(2);
expect(requestedShas[0]).not.toBe(requestedShas[1]);
});
it("passes forced PR status reads through to the GitHub service", async () => {
execFileSync("git", ["checkout", "-b", "feature"], { cwd: repoDir });
execFileSync("git", ["remote", "add", "origin", "https://github.com/getpaseo/paseo.git"], {
@@ -2637,6 +2680,41 @@ const x = 1;
}
});
it("keeps stale PR status when a Gitea-family refresh hits a transient command error", async () => {
execFileSync("git", ["checkout", "-b", "feature"], { cwd: repoDir });
execFileSync("git", ["remote", "add", "origin", "https://gitea.example.com/acme/repo.git"], {
cwd: repoDir,
});
__setPullRequestStatusCacheTtlForTests(50);
try {
let callCount = 0;
const service = createGitHubServiceForStatus(null);
service.getCurrentPullRequestStatus = async () => {
callCount += 1;
if (callCount === 1) {
return createPullRequestStatus({ url: "https://gitea.example.com/acme/repo/pulls/7" });
}
throw new TeaCommandError({
args: ["pr", "list"],
cwd: repoDir,
exitCode: 1,
stderr: "request timed out",
});
};
const fresh = await getPullRequestStatus(repoDir, service);
await sleep(80);
const stale = await getPullRequestStatus(repoDir, service);
expect(stale).toEqual(fresh);
expect(stale.status?.url).toContain("/pulls/7");
expect(callCount).toBe(2);
} finally {
__resetPullRequestStatusCacheForTests();
}
});
it("does not use stale PR status fallback for forced GitHub errors", async () => {
execFileSync("git", ["checkout", "-b", "feature"], { cwd: repoDir });
execFileSync("git", ["remote", "add", "origin", "https://github.com/getpaseo/paseo.git"], {