feat: support creating worktrees from GitHub pull requests
This commit is contained in:
@@ -1,22 +1,11 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { execSync } from "child_process";
|
||||
import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
mkdirSync,
|
||||
symlinkSync,
|
||||
} from "fs";
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync, readFileSync, realpathSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { tmpdir } from "os";
|
||||
import {
|
||||
__resetGhPathCacheForTests,
|
||||
__resetCheckoutShortstatCacheForTests,
|
||||
__resetPullRequestStatusCacheForTests,
|
||||
__setGhPathForTests,
|
||||
__setPullRequestStatusCacheTtlForTests,
|
||||
commitAll,
|
||||
getCachedCheckoutShortstat,
|
||||
@@ -38,9 +27,48 @@ import {
|
||||
parseStatusCheckRollup,
|
||||
isPaseoWorktreePath,
|
||||
isDescendantPath,
|
||||
searchGitHubIssuesAndPrs,
|
||||
warmCheckoutShortstatInBackground,
|
||||
} from "./checkout-git.js";
|
||||
import { createWorktree } from "./worktree.js";
|
||||
import {
|
||||
GitHubCliMissingError,
|
||||
type GitHubCurrentPullRequestStatus,
|
||||
type GitHubService,
|
||||
} from "../services/github-service.js";
|
||||
import {
|
||||
createWorktree as createWorktreePrimitive,
|
||||
type CreateWorktreeOptions,
|
||||
type WorktreeConfig,
|
||||
} from "./worktree.js";
|
||||
|
||||
interface LegacyCreateWorktreeTestOptions {
|
||||
branchName: string;
|
||||
cwd: string;
|
||||
baseBranch: string;
|
||||
worktreeSlug: string;
|
||||
runSetup?: boolean;
|
||||
paseoHome?: string;
|
||||
}
|
||||
|
||||
function createLegacyWorktreeForTest(
|
||||
options: CreateWorktreeOptions | LegacyCreateWorktreeTestOptions,
|
||||
): Promise<WorktreeConfig> {
|
||||
if ("source" in options) {
|
||||
return createWorktreePrimitive(options);
|
||||
}
|
||||
|
||||
return createWorktreePrimitive({
|
||||
cwd: options.cwd,
|
||||
worktreeSlug: options.worktreeSlug,
|
||||
source: {
|
||||
kind: "branch-off",
|
||||
baseBranch: options.baseBranch,
|
||||
newBranchName: options.branchName,
|
||||
},
|
||||
runSetup: options.runSetup ?? true,
|
||||
paseoHome: options.paseoHome,
|
||||
});
|
||||
}
|
||||
import { getPaseoWorktreeMetadataPath } from "./worktree-metadata.js";
|
||||
|
||||
function initRepo(): { tempDir: string; repoDir: string } {
|
||||
@@ -60,6 +88,52 @@ function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function createGitHubServiceForStatus(
|
||||
status: GitHubCurrentPullRequestStatus | null,
|
||||
options?: { onStatus?: () => void },
|
||||
): GitHubService {
|
||||
return {
|
||||
listPullRequests: async () => [],
|
||||
listIssues: async () => [],
|
||||
getPullRequest: async () => ({
|
||||
number: 1,
|
||||
title: "PR",
|
||||
url: "https://github.com/getpaseo/paseo/pull/1",
|
||||
state: "OPEN",
|
||||
body: null,
|
||||
baseRefName: "main",
|
||||
headRefName: "feature",
|
||||
labels: [],
|
||||
}),
|
||||
getPullRequestHeadRef: async () => "feature",
|
||||
getCurrentPullRequestStatus: async () => {
|
||||
options?.onStatus?.();
|
||||
return status;
|
||||
},
|
||||
createPullRequest: async () => ({
|
||||
url: "https://github.com/getpaseo/paseo/pull/1",
|
||||
number: 1,
|
||||
}),
|
||||
isAuthenticated: async () => true,
|
||||
invalidate: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
function createPullRequestStatus(overrides?: Partial<GitHubCurrentPullRequestStatus>) {
|
||||
return {
|
||||
url: "https://github.com/getpaseo/paseo/pull/123",
|
||||
title: "Ship feature",
|
||||
state: "open",
|
||||
baseRefName: "main",
|
||||
headRefName: "feature",
|
||||
isMerged: false,
|
||||
checks: [],
|
||||
checksStatus: "none" as const,
|
||||
reviewDecision: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("checkout git utilities", () => {
|
||||
let tempDir: string;
|
||||
let repoDir: string;
|
||||
@@ -70,13 +144,11 @@ describe("checkout git utilities", () => {
|
||||
tempDir = setup.tempDir;
|
||||
repoDir = setup.repoDir;
|
||||
paseoHome = join(tempDir, "paseo-home");
|
||||
__resetGhPathCacheForTests();
|
||||
__resetCheckoutShortstatCacheForTests();
|
||||
__resetPullRequestStatusCacheForTests();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
__resetGhPathCacheForTests();
|
||||
__resetCheckoutShortstatCacheForTests();
|
||||
__resetPullRequestStatusCacheForTests();
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
@@ -383,7 +455,7 @@ const x = 1;
|
||||
});
|
||||
|
||||
it("handles status/diff/commit in a .paseo worktree", async () => {
|
||||
const result = await createWorktree({
|
||||
const result = await createLegacyWorktreeForTest({
|
||||
branchName: "main",
|
||||
cwd: repoDir,
|
||||
baseBranch: "main",
|
||||
@@ -417,7 +489,7 @@ const x = 1;
|
||||
});
|
||||
|
||||
it("returns checkout root metadata for .paseo worktrees", async () => {
|
||||
const result = await createWorktree({
|
||||
const result = await createLegacyWorktreeForTest({
|
||||
branchName: "main",
|
||||
cwd: repoDir,
|
||||
baseBranch: "main",
|
||||
@@ -444,7 +516,7 @@ const x = 1;
|
||||
execSync("git config user.email 'test@test.com'", { cwd: mainCheckoutDir });
|
||||
execSync("git config user.name 'Test'", { cwd: mainCheckoutDir });
|
||||
|
||||
const worktree = await createWorktree({
|
||||
const worktree = await createLegacyWorktreeForTest({
|
||||
branchName: "feature",
|
||||
cwd: mainCheckoutDir,
|
||||
baseBranch: "main",
|
||||
@@ -459,7 +531,7 @@ const x = 1;
|
||||
});
|
||||
|
||||
it("merges the current branch into base from a worktree checkout", async () => {
|
||||
const worktree = await createWorktree({
|
||||
const worktree = await createLegacyWorktreeForTest({
|
||||
branchName: "main",
|
||||
cwd: repoDir,
|
||||
baseBranch: "main",
|
||||
@@ -720,20 +792,130 @@ const x = 1;
|
||||
it("disables GitHub features when gh is unavailable", async () => {
|
||||
execSync("git remote add origin https://github.com/getpaseo/paseo.git", { cwd: repoDir });
|
||||
|
||||
const fakeBinDir = join(tempDir, "fake-bin");
|
||||
mkdirSync(fakeBinDir);
|
||||
const gitPath = execSync("command -v git", { stdio: "pipe" }).toString().trim();
|
||||
symlinkSync(gitPath, join(fakeBinDir, "git"));
|
||||
const github = createGitHubServiceForStatus(null);
|
||||
github.getCurrentPullRequestStatus = async () => {
|
||||
throw new GitHubCliMissingError();
|
||||
};
|
||||
const status = await getPullRequestStatus(repoDir, github);
|
||||
expect(status.githubFeaturesEnabled).toBe(false);
|
||||
expect(status.status).toBeNull();
|
||||
});
|
||||
|
||||
try {
|
||||
__setGhPathForTests(null);
|
||||
const status = await getPullRequestStatus(repoDir);
|
||||
expect(status.githubFeaturesEnabled).toBe(false);
|
||||
expect(status.status).toBeNull();
|
||||
} finally {
|
||||
__resetGhPathCacheForTests();
|
||||
__resetPullRequestStatusCacheForTests();
|
||||
}
|
||||
it("searches GitHub issues and PRs through the GitHub service", async () => {
|
||||
let issueCalls = 0;
|
||||
let pullRequestCalls = 0;
|
||||
const github = createGitHubServiceForStatus(null);
|
||||
github.listIssues = async (options) => {
|
||||
issueCalls += 1;
|
||||
expect(options).toEqual({ cwd: repoDir, query: "cache", limit: 5 });
|
||||
return [
|
||||
{
|
||||
number: 55,
|
||||
title: "Issue title",
|
||||
url: "https://github.com/getpaseo/paseo/issues/55",
|
||||
state: "OPEN",
|
||||
body: "issue body",
|
||||
labels: ["bug"],
|
||||
},
|
||||
];
|
||||
};
|
||||
github.listPullRequests = async (options) => {
|
||||
pullRequestCalls += 1;
|
||||
expect(options).toEqual({ cwd: repoDir, query: "cache", limit: 5 });
|
||||
return [
|
||||
{
|
||||
number: 123,
|
||||
title: "PR title",
|
||||
url: "https://github.com/getpaseo/paseo/pull/123",
|
||||
state: "OPEN",
|
||||
body: "pr body",
|
||||
baseRefName: "main",
|
||||
headRefName: "feature",
|
||||
labels: ["enhancement"],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const result = await searchGitHubIssuesAndPrs(repoDir, "cache", 5, github);
|
||||
|
||||
expect(issueCalls).toBe(1);
|
||||
expect(pullRequestCalls).toBe(1);
|
||||
expect(result).toEqual({
|
||||
githubFeaturesEnabled: true,
|
||||
items: [
|
||||
{
|
||||
kind: "issue",
|
||||
number: 55,
|
||||
title: "Issue title",
|
||||
url: "https://github.com/getpaseo/paseo/issues/55",
|
||||
state: "OPEN",
|
||||
body: "issue body",
|
||||
labels: ["bug"],
|
||||
baseRefName: null,
|
||||
headRefName: null,
|
||||
},
|
||||
{
|
||||
kind: "pr",
|
||||
number: 123,
|
||||
title: "PR title",
|
||||
url: "https://github.com/getpaseo/paseo/pull/123",
|
||||
state: "OPEN",
|
||||
body: "pr body",
|
||||
labels: ["enhancement"],
|
||||
baseRefName: "main",
|
||||
headRefName: "feature",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("searches only GitHub PRs when the search kinds request excludes issues", async () => {
|
||||
let issueCalls = 0;
|
||||
let pullRequestCalls = 0;
|
||||
const github = createGitHubServiceForStatus(null);
|
||||
github.listIssues = async () => {
|
||||
issueCalls += 1;
|
||||
return [];
|
||||
};
|
||||
github.listPullRequests = async (options) => {
|
||||
pullRequestCalls += 1;
|
||||
expect(options).toEqual({ cwd: repoDir, query: "cache", limit: 5 });
|
||||
return [
|
||||
{
|
||||
number: 123,
|
||||
title: "PR title",
|
||||
url: "https://github.com/getpaseo/paseo/pull/123",
|
||||
state: "OPEN",
|
||||
body: "pr body",
|
||||
baseRefName: "main",
|
||||
headRefName: "feature",
|
||||
labels: ["enhancement"],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const result = await searchGitHubIssuesAndPrs(repoDir, "cache", 5, github, {
|
||||
kinds: ["github-pr"],
|
||||
});
|
||||
|
||||
expect(issueCalls).toBe(0);
|
||||
expect(pullRequestCalls).toBe(1);
|
||||
expect(result).toEqual({
|
||||
githubFeaturesEnabled: true,
|
||||
items: [
|
||||
{
|
||||
kind: "pr",
|
||||
number: 123,
|
||||
title: "PR title",
|
||||
url: "https://github.com/getpaseo/paseo/pull/123",
|
||||
state: "OPEN",
|
||||
body: "pr body",
|
||||
labels: ["enhancement"],
|
||||
baseRefName: "main",
|
||||
headRefName: "feature",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("parses real gh status check rollup output and dedupes by latest check run", () => {
|
||||
@@ -835,189 +1017,89 @@ const x = 1;
|
||||
execSync("git checkout -b feature", { cwd: repoDir });
|
||||
execSync("git remote add origin https://github.com/getpaseo/paseo.git", { cwd: repoDir });
|
||||
|
||||
const fakeBinDir = join(tempDir, "fake-bin-gh-merged");
|
||||
mkdirSync(fakeBinDir);
|
||||
const gitPath = execSync("command -v git", { stdio: "pipe" }).toString().trim();
|
||||
symlinkSync(gitPath, join(fakeBinDir, "git"));
|
||||
writeFileSync(
|
||||
join(fakeBinDir, "gh"),
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'if [[ "${1-}" == "--version" ]]; then',
|
||||
' echo "gh version 2.0.0"',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'args="$*"',
|
||||
'if [[ "$args" == "pr view --json url,title,state,baseRefName,headRefName,mergedAt,statusCheckRollup,reviewDecision" ]]; then',
|
||||
' echo \'{"url":"https://github.com/getpaseo/paseo/pull/123","title":"Ship feature","state":"closed","baseRefName":"main","headRefName":"feature","mergedAt":"2026-02-18T00:00:00Z","statusCheckRollup":[],"reviewDecision":""}\'',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'echo "unexpected gh args: $args" >&2',
|
||||
"exit 1",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
const status = await getPullRequestStatus(
|
||||
repoDir,
|
||||
createGitHubServiceForStatus(
|
||||
createPullRequestStatus({
|
||||
state: "merged",
|
||||
isMerged: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
execSync(`chmod +x ${join(fakeBinDir, "gh")}`);
|
||||
|
||||
try {
|
||||
__setGhPathForTests(join(fakeBinDir, "gh"));
|
||||
const status = await getPullRequestStatus(repoDir);
|
||||
expect(status.githubFeaturesEnabled).toBe(true);
|
||||
expect(status.status).not.toBeNull();
|
||||
expect(status.status?.url).toContain("/pull/123");
|
||||
expect(status.status?.baseRefName).toBe("main");
|
||||
expect(status.status?.headRefName).toBe("feature");
|
||||
expect(status.status?.isMerged).toBe(true);
|
||||
expect(status.status?.state).toBe("merged");
|
||||
} finally {
|
||||
__resetGhPathCacheForTests();
|
||||
__resetPullRequestStatusCacheForTests();
|
||||
}
|
||||
expect(status.githubFeaturesEnabled).toBe(true);
|
||||
expect(status.status).not.toBeNull();
|
||||
expect(status.status?.url).toContain("/pull/123");
|
||||
expect(status.status?.baseRefName).toBe("main");
|
||||
expect(status.status?.headRefName).toBe("feature");
|
||||
expect(status.status?.isMerged).toBe(true);
|
||||
expect(status.status?.state).toBe("merged");
|
||||
});
|
||||
|
||||
it("returns closed-unmerged PR status without marking it as merged", async () => {
|
||||
execSync("git checkout -b feature", { cwd: repoDir });
|
||||
execSync("git remote add origin https://github.com/getpaseo/paseo.git", { cwd: repoDir });
|
||||
|
||||
const fakeBinDir = join(tempDir, "fake-bin-gh-closed");
|
||||
mkdirSync(fakeBinDir);
|
||||
const gitPath = execSync("command -v git", { stdio: "pipe" }).toString().trim();
|
||||
symlinkSync(gitPath, join(fakeBinDir, "git"));
|
||||
writeFileSync(
|
||||
join(fakeBinDir, "gh"),
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'if [[ "${1-}" == "--version" ]]; then',
|
||||
' echo "gh version 2.0.0"',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'args="$*"',
|
||||
'if [[ "$args" == "pr view --json url,title,state,baseRefName,headRefName,mergedAt,statusCheckRollup,reviewDecision" ]]; then',
|
||||
' echo \'{"url":"https://github.com/getpaseo/paseo/pull/999","title":"Closed without merge","state":"closed","baseRefName":"main","headRefName":"feature","mergedAt":null,"statusCheckRollup":[],"reviewDecision":""}\'',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'echo "unexpected gh args: $args" >&2',
|
||||
"exit 1",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
const status = await getPullRequestStatus(
|
||||
repoDir,
|
||||
createGitHubServiceForStatus(
|
||||
createPullRequestStatus({
|
||||
url: "https://github.com/getpaseo/paseo/pull/999",
|
||||
title: "Closed without merge",
|
||||
state: "closed",
|
||||
}),
|
||||
),
|
||||
);
|
||||
execSync(`chmod +x ${join(fakeBinDir, "gh")}`);
|
||||
|
||||
try {
|
||||
__setGhPathForTests(join(fakeBinDir, "gh"));
|
||||
const status = await getPullRequestStatus(repoDir);
|
||||
expect(status.githubFeaturesEnabled).toBe(true);
|
||||
expect(status.status).not.toBeNull();
|
||||
expect(status.status?.url).toContain("/pull/999");
|
||||
expect(status.status?.baseRefName).toBe("main");
|
||||
expect(status.status?.headRefName).toBe("feature");
|
||||
expect(status.status?.isMerged).toBe(false);
|
||||
expect(status.status?.state).toBe("closed");
|
||||
} finally {
|
||||
__resetGhPathCacheForTests();
|
||||
__resetPullRequestStatusCacheForTests();
|
||||
}
|
||||
expect(status.githubFeaturesEnabled).toBe(true);
|
||||
expect(status.status).not.toBeNull();
|
||||
expect(status.status?.url).toContain("/pull/999");
|
||||
expect(status.status?.baseRefName).toBe("main");
|
||||
expect(status.status?.headRefName).toBe("feature");
|
||||
expect(status.status?.isMerged).toBe(false);
|
||||
expect(status.status?.state).toBe("closed");
|
||||
});
|
||||
|
||||
it("caches PR status results for duplicate lookups", async () => {
|
||||
execSync("git checkout -b feature", { cwd: repoDir });
|
||||
execSync("git remote add origin https://github.com/getpaseo/paseo.git", { cwd: repoDir });
|
||||
|
||||
const fakeBinDir = join(tempDir, "fake-bin-gh-cache-hit");
|
||||
const callCountPath = join(tempDir, "gh-call-count.txt");
|
||||
mkdirSync(fakeBinDir);
|
||||
const gitPath = execSync("command -v git", { stdio: "pipe" }).toString().trim();
|
||||
symlinkSync(gitPath, join(fakeBinDir, "git"));
|
||||
writeFileSync(callCountPath, "0\n");
|
||||
writeFileSync(
|
||||
join(fakeBinDir, "gh"),
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
`count_file=${JSON.stringify(callCountPath)}`,
|
||||
'if [[ "${1-}" == "--version" ]]; then',
|
||||
' echo "gh version 2.0.0"',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'args="$*"',
|
||||
'if [[ "$args" == "pr view --json url,title,state,baseRefName,headRefName,mergedAt,statusCheckRollup,reviewDecision" ]]; then',
|
||||
' count="$(cat "$count_file")"',
|
||||
' printf "%s\\n" "$((count + 1))" > "$count_file"',
|
||||
' echo \'{"url":"https://github.com/getpaseo/paseo/pull/123","title":"Ship feature","state":"OPEN","baseRefName":"main","headRefName":"feature","mergedAt":null,"statusCheckRollup":[],"reviewDecision":""}\'',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'echo "unexpected gh args: $args" >&2',
|
||||
"exit 1",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
execSync(`chmod +x ${join(fakeBinDir, "gh")}`);
|
||||
|
||||
try {
|
||||
__setGhPathForTests(join(fakeBinDir, "gh"));
|
||||
const first = await getPullRequestStatus(repoDir);
|
||||
const second = await getPullRequestStatus(repoDir);
|
||||
expect(first).toEqual(second);
|
||||
expect(first.status?.url).toContain("/pull/123");
|
||||
expect(readFileSync(callCountPath, "utf8").trim()).toBe("1");
|
||||
} finally {
|
||||
__resetGhPathCacheForTests();
|
||||
__resetPullRequestStatusCacheForTests();
|
||||
}
|
||||
let callCount = 0;
|
||||
const github = createGitHubServiceForStatus(createPullRequestStatus(), {
|
||||
onStatus: () => {
|
||||
callCount += 1;
|
||||
},
|
||||
});
|
||||
const first = await getPullRequestStatus(repoDir, github);
|
||||
const second = await getPullRequestStatus(repoDir, github);
|
||||
expect(first).toEqual(second);
|
||||
expect(first.status?.url).toContain("/pull/123");
|
||||
expect(callCount).toBe(1);
|
||||
});
|
||||
|
||||
it("expires cached PR status after the TTL", async () => {
|
||||
execSync("git checkout -b feature", { cwd: repoDir });
|
||||
execSync("git remote add origin https://github.com/getpaseo/paseo.git", { cwd: repoDir });
|
||||
|
||||
const fakeBinDir = join(tempDir, "fake-bin-gh-cache-expiry");
|
||||
const callCountPath = join(tempDir, "gh-call-count-expiry.txt");
|
||||
mkdirSync(fakeBinDir);
|
||||
const gitPath = execSync("command -v git", { stdio: "pipe" }).toString().trim();
|
||||
symlinkSync(gitPath, join(fakeBinDir, "git"));
|
||||
writeFileSync(callCountPath, "0\n");
|
||||
writeFileSync(
|
||||
join(fakeBinDir, "gh"),
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
`count_file=${JSON.stringify(callCountPath)}`,
|
||||
'if [[ "${1-}" == "--version" ]]; then',
|
||||
' echo "gh version 2.0.0"',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'args="$*"',
|
||||
'if [[ "$args" == "pr view --json url,title,state,baseRefName,headRefName,mergedAt,statusCheckRollup,reviewDecision" ]]; then',
|
||||
' count="$(cat "$count_file")"',
|
||||
' next="$((count + 1))"',
|
||||
' printf "%s\\n" "$next" > "$count_file"',
|
||||
' printf \'{"url":"https://github.com/getpaseo/paseo/pull/%s","title":"Ship feature","state":"OPEN","baseRefName":"main","headRefName":"feature","mergedAt":null,"statusCheckRollup":[],"reviewDecision":""}\\n\' "$next"',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'echo "unexpected gh args: $args" >&2',
|
||||
"exit 1",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
execSync(`chmod +x ${join(fakeBinDir, "gh")}`);
|
||||
|
||||
__setPullRequestStatusCacheTtlForTests(50);
|
||||
try {
|
||||
__setGhPathForTests(join(fakeBinDir, "gh"));
|
||||
const first = await getPullRequestStatus(repoDir);
|
||||
let callCount = 0;
|
||||
const github = createGitHubServiceForStatus(null, {
|
||||
onStatus: () => {
|
||||
callCount += 1;
|
||||
},
|
||||
});
|
||||
github.getCurrentPullRequestStatus = async () => {
|
||||
callCount += 1;
|
||||
return createPullRequestStatus({
|
||||
url: `https://github.com/getpaseo/paseo/pull/${callCount}`,
|
||||
});
|
||||
};
|
||||
const first = await getPullRequestStatus(repoDir, github);
|
||||
await sleep(80);
|
||||
const second = await getPullRequestStatus(repoDir);
|
||||
const second = await getPullRequestStatus(repoDir, github);
|
||||
expect(first.status?.url).toContain("/pull/1");
|
||||
expect(second.status?.url).toContain("/pull/2");
|
||||
expect(readFileSync(callCountPath, "utf8").trim()).toBe("2");
|
||||
expect(callCount).toBe(2);
|
||||
} finally {
|
||||
__resetGhPathCacheForTests();
|
||||
__resetPullRequestStatusCacheForTests();
|
||||
}
|
||||
});
|
||||
@@ -1026,50 +1108,18 @@ const x = 1;
|
||||
execSync("git checkout -b feature", { cwd: repoDir });
|
||||
execSync("git remote add origin https://github.com/getpaseo/paseo.git", { cwd: repoDir });
|
||||
|
||||
const fakeBinDir = join(tempDir, "fake-bin-gh-cache-concurrent");
|
||||
const callCountPath = join(tempDir, "gh-call-count-concurrent.txt");
|
||||
mkdirSync(fakeBinDir);
|
||||
const gitPath = execSync("command -v git", { stdio: "pipe" }).toString().trim();
|
||||
symlinkSync(gitPath, join(fakeBinDir, "git"));
|
||||
writeFileSync(callCountPath, "0\n");
|
||||
writeFileSync(
|
||||
join(fakeBinDir, "gh"),
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
`count_file=${JSON.stringify(callCountPath)}`,
|
||||
'if [[ "${1-}" == "--version" ]]; then',
|
||||
' echo "gh version 2.0.0"',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'args="$*"',
|
||||
'if [[ "$args" == "pr view --json url,title,state,baseRefName,headRefName,mergedAt,statusCheckRollup,reviewDecision" ]]; then',
|
||||
' count="$(cat "$count_file")"',
|
||||
' printf "%s\\n" "$((count + 1))" > "$count_file"',
|
||||
" sleep 0.2",
|
||||
' echo \'{"url":"https://github.com/getpaseo/paseo/pull/123","title":"Ship feature","state":"OPEN","baseRefName":"main","headRefName":"feature","mergedAt":null,"statusCheckRollup":[],"reviewDecision":""}\'',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'echo "unexpected gh args: $args" >&2',
|
||||
"exit 1",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
execSync(`chmod +x ${join(fakeBinDir, "gh")}`);
|
||||
|
||||
try {
|
||||
__setGhPathForTests(join(fakeBinDir, "gh"));
|
||||
const [first, second] = await Promise.all([
|
||||
getPullRequestStatus(repoDir),
|
||||
getPullRequestStatus(repoDir),
|
||||
]);
|
||||
expect(first).toEqual(second);
|
||||
expect(readFileSync(callCountPath, "utf8").trim()).toBe("1");
|
||||
} finally {
|
||||
__resetGhPathCacheForTests();
|
||||
__resetPullRequestStatusCacheForTests();
|
||||
}
|
||||
let callCount = 0;
|
||||
const github = createGitHubServiceForStatus(createPullRequestStatus(), {
|
||||
onStatus: () => {
|
||||
callCount += 1;
|
||||
},
|
||||
});
|
||||
const [first, second] = await Promise.all([
|
||||
getPullRequestStatus(repoDir, github),
|
||||
getPullRequestStatus(repoDir, github),
|
||||
]);
|
||||
expect(first).toEqual(second);
|
||||
expect(callCount).toBe(1);
|
||||
});
|
||||
|
||||
it("returns typed MergeConflictError on merge conflicts", async () => {
|
||||
@@ -1110,7 +1160,7 @@ const x = 1;
|
||||
execSync("git checkout main", { cwd: repoDir });
|
||||
|
||||
// Create a worktree/branch based on develop, but keep main as the repo default.
|
||||
const worktree = await createWorktree({
|
||||
const worktree = await createLegacyWorktreeForTest({
|
||||
branchName: "feature",
|
||||
cwd: repoDir,
|
||||
baseBranch: "develop",
|
||||
@@ -1156,7 +1206,7 @@ const x = 1;
|
||||
execSync("git checkout main", { cwd: repoDir });
|
||||
|
||||
// Create a Paseo worktree configured to use develop as base.
|
||||
const worktree = await createWorktree({
|
||||
const worktree = await createLegacyWorktreeForTest({
|
||||
branchName: "feature",
|
||||
cwd: repoDir,
|
||||
baseBranch: "develop",
|
||||
@@ -1189,7 +1239,7 @@ const x = 1;
|
||||
});
|
||||
|
||||
it("throws if Paseo worktree base metadata is missing", async () => {
|
||||
const worktree = await createWorktree({
|
||||
const worktree = await createLegacyWorktreeForTest({
|
||||
branchName: "main",
|
||||
cwd: repoDir,
|
||||
baseBranch: "main",
|
||||
|
||||
Reference in New Issue
Block a user