Skip to content

Commit 7426107

Browse files
committed
use using
1 parent 142abae commit 7426107

3 files changed

Lines changed: 146 additions & 117 deletions

File tree

src/pr-status/message.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,20 @@ import {
1010
getNewChangesetTemplateContent,
1111
getNewChangesetUrl,
1212
} from "./template.ts";
13-
import { withPullRequestWorktree } from "./worktree.ts";
13+
import { getPullRequestWorktree } from "./worktree.ts";
1414

1515
type PullRequestContext = NonNullable<
1616
typeof github.context.payload.pull_request
1717
>;
1818

1919
export async function getCommentMessage(context: PullRequestContext) {
20-
const { releasePlan, templateContent } = await withPullRequestWorktree(
21-
context,
22-
async ({ cwd, baseRef }) => {
23-
const releasePlan = await getReleasePlan(cwd, baseRef);
24-
const templateContent = await getNewChangesetTemplateContent(
25-
cwd,
26-
baseRef,
27-
context.title,
28-
);
29-
30-
return {
31-
releasePlan,
32-
templateContent,
33-
};
34-
},
20+
await using worktree = await getPullRequestWorktree(context);
21+
22+
const releasePlan = await getReleasePlan(worktree.cwd, worktree.baseRef);
23+
const templateContent = await getNewChangesetTemplateContent(
24+
worktree.cwd,
25+
worktree.baseRef,
26+
context.title,
3527
);
3628

3729
const newChangesetUrl = getNewChangesetUrl(

src/pr-status/worktree.test.ts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { pathToFileURL } from "node:url";
2+
import type * as github from "@actions/github";
23
import getReleasePlan from "@changesets/get-release-plan";
34
import { createFixture } from "fs-fixture";
45
import { exec } from "tinyexec";
56
import { describe, expect, it } from "vitest";
6-
import { withPullRequestWorktree } from "./worktree.ts";
7+
import { getPullRequestWorktree } from "./worktree.ts";
8+
9+
type PullRequestContext = NonNullable<
10+
typeof github.context.payload.pull_request
11+
>;
712

813
async function git(cwd: string, args: string[]) {
914
const output = await exec("git", args, {
@@ -13,7 +18,7 @@ async function git(cwd: string, args: string[]) {
1318
return output.stdout.trim();
1419
}
1520

16-
describe("withPullRequestWorktree", () => {
21+
describe("getPullRequestWorktree", () => {
1722
it("fetches a PR branch into a detached worktree and keeps the main checkout untouched", async () => {
1823
// Local source repo
1924
await using sourceRepoFixture = await createFixture({
@@ -107,41 +112,44 @@ Add pkg-a
107112
number: 123,
108113
base: {
109114
ref: "main",
115+
repo: {
116+
clone_url: pathToFileURL(originBare).toString(),
117+
},
110118
},
111119
head: {
112120
ref: "feature",
113121
repo: {
114122
clone_url: pathToFileURL(forkBare).toString(),
115123
},
116124
},
117-
} as any;
118-
119-
const result = await withPullRequestWorktree(
120-
context,
121-
async ({ cwd, baseRef }) => {
122-
const releasePlan = await getReleasePlan(cwd, baseRef);
123-
return {
124-
currentHead: await git(cwd, ["rev-parse", "HEAD"]),
125-
currentBranch: await git(cwd, ["branch", "--show-current"]),
126-
releases: releasePlan.releases.map((release) => ({
127-
name: release.name,
128-
type: release.type,
129-
newVersion: release.newVersion,
130-
})),
131-
};
132-
},
125+
};
126+
127+
await using worktree = await getPullRequestWorktree(
128+
context satisfies PullRequestContext as PullRequestContext,
133129
checkoutRepo,
134130
);
135131

136-
expect(result.currentHead).not.toBe(originalHead);
137-
expect(result.currentBranch).toBe("");
138-
expect(result.releases).toEqual([
132+
const releasePlan = await getReleasePlan(worktree.cwd, worktree.baseRef);
133+
134+
const currentHead = await git(worktree.cwd, ["rev-parse", "HEAD"]);
135+
expect(currentHead).not.toBe(originalHead);
136+
137+
const currentBranch = await git(worktree.cwd, ["branch", "--show-current"]);
138+
expect(currentBranch).toBe("");
139+
140+
const releases = releasePlan.releases.map((release) => ({
141+
name: release.name,
142+
type: release.type,
143+
newVersion: release.newVersion,
144+
}));
145+
expect(releases).toEqual([
139146
{
140147
name: "pkg-a",
141148
type: "patch",
142149
newVersion: "1.0.1",
143150
},
144151
]);
152+
145153
expect(await git(checkoutRepo, ["rev-parse", "HEAD"])).toBe(originalHead);
146154
expect(await git(checkoutRepo, ["branch", "--show-current"])).toBe("main");
147155
});

src/pr-status/worktree.ts

Lines changed: 109 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -25,112 +25,141 @@ function git(cwd: string, args: string[], opts: TinyexecOptions = {}) {
2525
});
2626
}
2727

28-
async function deleteRef(cwd: string, ref: string) {
29-
await git(cwd, ["update-ref", "-d", ref], { throwOnError: false });
28+
interface Ref {
29+
fetchSource: string;
30+
local: string;
31+
remote: string;
3032
}
3133

32-
function getRefNames(context: PullRequestContext) {
34+
function getRefs(context: PullRequestContext): Record<"base" | "head", Ref> {
3335
const suffix = `${context.number}-${randomUUID()}`;
3436
return {
35-
baseLocalRef: `refs/changesets-action-pr-status/base/${suffix}`,
36-
baseRemoteRef: `refs/heads/${context.base.ref}`,
37-
headLocalRef: `refs/changesets-action-pr-status/head/${suffix}`,
38-
headRemoteRef: `refs/heads/${context.head.ref}`,
37+
base: {
38+
fetchSource: "origin",
39+
local: `refs/changesets-action-pr-status/base/${suffix}`,
40+
remote: `refs/heads/${context.base.ref}`,
41+
},
42+
head: {
43+
fetchSource: context.head.repo.clone_url,
44+
local: `refs/changesets-action-pr-status/head/${suffix}`,
45+
remote: `refs/heads/${context.head.ref}`,
46+
},
3947
};
4048
}
4149

50+
async function deepenRef(cwd: string, ref: Ref, deepenBy: number) {
51+
await git(cwd, [
52+
"fetch",
53+
"--no-tags",
54+
`--deepen=${deepenBy}`,
55+
ref.fetchSource,
56+
`${ref.remote}:${ref.local}`,
57+
]);
58+
}
59+
4260
async function ensureMergeBase(args: {
4361
cwd: string;
44-
refs: ReturnType<typeof getRefNames>;
45-
headRemoteUrl: string;
62+
refs: ReturnType<typeof getRefs>;
4663
deepenBy?: number;
4764
}) {
48-
const { cwd, refs, headRemoteUrl, deepenBy = 50 } = args;
65+
const { cwd, refs, deepenBy = 50 } = args;
4966

5067
while (true) {
51-
const mergeBase = await git(
52-
cwd,
53-
["merge-base", refs.baseLocalRef, "HEAD"],
54-
{ throwOnError: false },
55-
);
68+
const mergeBase = await git(cwd, ["merge-base", refs.base.local, "HEAD"], {
69+
throwOnError: false,
70+
});
5671

5772
if (mergeBase.exitCode === 0) {
5873
return mergeBase.stdout.trim();
5974
}
6075

6176
if (!(await isRepoShallow({ cwd }))) {
6277
throw new Error(
63-
`Failed to find merge base between "${refs.baseLocalRef}" and HEAD, and the repository is no longer shallow.`,
78+
`Failed to find merge base between "${refs.base.local}" and HEAD, and the repository is no longer shallow.`,
6479
);
6580
}
6681

67-
await git(cwd, [
68-
"fetch",
69-
"--no-tags",
70-
`--deepen=${deepenBy}`,
71-
"origin",
72-
`${refs.baseRemoteRef}:${refs.baseLocalRef}`,
73-
]);
74-
await git(cwd, [
75-
"fetch",
76-
"--no-tags",
77-
`--deepen=${deepenBy}`,
78-
headRemoteUrl,
79-
`${refs.headRemoteRef}:${refs.headLocalRef}`,
80-
]);
82+
await deepenRef(cwd, refs.base, deepenBy);
83+
await deepenRef(cwd, refs.head, deepenBy);
8184
}
8285
}
8386

84-
export async function withPullRequestWorktree<T>(
87+
async function mkdtemp(prefix: string) {
88+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
89+
90+
return {
91+
dir,
92+
async [Symbol.asyncDispose]() {
93+
await fs.rm(dir, { recursive: true, force: true });
94+
},
95+
};
96+
}
97+
98+
async function tempRef(cwd: string, ref: Ref) {
99+
await git(cwd, [
100+
"fetch",
101+
"--no-tags",
102+
"--depth=1",
103+
ref.fetchSource,
104+
`${ref.remote}:${ref.local}`,
105+
]);
106+
return {
107+
async [Symbol.asyncDispose]() {
108+
await git(cwd, ["update-ref", "-d", ref.local], { throwOnError: false });
109+
},
110+
};
111+
}
112+
113+
async function tempWorktree(cwd: string, dir: string, ref: Ref) {
114+
await git(cwd, ["worktree", "add", "--detach", dir, ref.local]);
115+
116+
return {
117+
async [Symbol.asyncDispose]() {
118+
await git(cwd, ["worktree", "remove", "--force", dir], {
119+
throwOnError: false,
120+
});
121+
},
122+
};
123+
}
124+
125+
type WithAsyncDispose<T> = T & {
126+
[Symbol.asyncDispose](): Promise<void>;
127+
};
128+
129+
function moveDisposable<T extends object>(
130+
stack: AsyncDisposableStack,
131+
value: T,
132+
): WithAsyncDispose<T> {
133+
const moved = stack.move();
134+
return Object.assign(value, {
135+
async [Symbol.asyncDispose]() {
136+
await moved.disposeAsync();
137+
},
138+
});
139+
}
140+
141+
export async function getPullRequestWorktree(
85142
context: PullRequestContext,
86-
fn: (worktree: WorktreeInfo) => Promise<T>,
87-
repoCwd: string = process.cwd(),
88-
) {
89-
const worktreeDir = await fs.mkdtemp(
90-
path.join(os.tmpdir(), "changesets-action-pr-status-"),
91-
);
92-
const refs = getRefNames(context);
93-
94-
try {
95-
await git(repoCwd, [
96-
"fetch",
97-
"--no-tags",
98-
"--depth=1",
99-
"origin",
100-
`${refs.baseRemoteRef}:${refs.baseLocalRef}`,
101-
]);
102-
await git(repoCwd, [
103-
"fetch",
104-
"--no-tags",
105-
"--depth=1",
106-
context.head.repo.clone_url,
107-
`${refs.headRemoteRef}:${refs.headLocalRef}`,
108-
]);
109-
await git(repoCwd, [
110-
"worktree",
111-
"add",
112-
"--detach",
113-
worktreeDir,
114-
refs.headLocalRef,
115-
]);
116-
await ensureMergeBase({
117-
cwd: worktreeDir,
118-
refs,
119-
headRemoteUrl: context.head.repo.clone_url,
120-
});
143+
cwd: string = process.cwd(),
144+
): Promise<WithAsyncDispose<WorktreeInfo>> {
145+
await using stack = new AsyncDisposableStack();
146+
const worktreeDir = stack.use(
147+
await mkdtemp("changesets-action-pr-status-"),
148+
).dir;
121149

122-
return await fn({
123-
baseRef: refs.baseLocalRef,
124-
cwd: worktreeDir,
125-
});
126-
} finally {
127-
await git(repoCwd, ["worktree", "remove", "--force", worktreeDir], {
128-
throwOnError: false,
129-
});
130-
await Promise.all([
131-
deleteRef(repoCwd, refs.baseLocalRef),
132-
deleteRef(repoCwd, refs.headLocalRef),
133-
]);
134-
await fs.rm(worktreeDir, { recursive: true, force: true });
135-
}
150+
const refs = getRefs(context);
151+
152+
stack.use(await tempRef(cwd, refs.base));
153+
stack.use(await tempRef(cwd, refs.head));
154+
stack.use(await tempWorktree(cwd, worktreeDir, refs.head));
155+
156+
await ensureMergeBase({
157+
cwd: worktreeDir,
158+
refs,
159+
});
160+
161+
return moveDisposable(stack, {
162+
baseRef: refs.base.local,
163+
cwd: worktreeDir,
164+
});
136165
}

0 commit comments

Comments
 (0)