Skip to content
File

Blob: src/worker/dispatch/shared/run-environment/git/commands.ts

typescript215 lines
1import type { ExecutionSession } from "@cloudflare/sandbox";
2 
3import { CommitSha, type CommitSha as CommitShaType } from "@/contracts";
4import { GIT_AUTH_HEADER_ENV } from "@/worker/sandbox/git";
5 
6const quoteShellArg = (value: string): string => `'${value.replace(/'/gu, `'\"'\"'`)}'`;
7const SHALLOW_UNSUPPORTED_ERROR_PATTERNS = [
8 /does not support shallow requests/u,
9 /does not support shallow capabilities/u,
10 /does not support shallow/u,
11 /does not support --depth/u,
12] as const;
13
14const buildGitCommandPrefix = (hasAuthHeader: boolean): string =>
15 hasAuthHeader
16 ? `git -c credential.helper= -c "http.extraHeader=$${GIT_AUTH_HEADER_ENV}"`
17 : "git -c credential.helper=";
18
19const isShallowUnsupportedError = (stderr: string): boolean =>
20 SHALLOW_UNSUPPORTED_ERROR_PATTERNS.some((pattern) => pattern.test(stderr.toLowerCase()));
21
22const hasCommitObject = async (
23 session: ExecutionSession,
24 repoRoot: string,
25 commitSha: CommitShaType,
26): Promise<boolean> => {
27 const result = await session.exec(
28 `git -C ${quoteShellArg(repoRoot)} cat-file -e ${quoteShellArg(`${commitSha}^{commit}`)}`,
29 {
30 cwd: repoRoot,
31 },
32 );
33
34 return result.success;
35};
36
37const fetchRef = async (
38 session: ExecutionSession,
39 repoRoot: string,
40 ref: string,
41 hasAuthHeader: boolean,
42 depth?: number,
43 errorMessage?: string,
44): Promise<void> => {
45 const depthArgs = depth === undefined ? "" : ` --depth=${depth} --update-shallow`;
46 let result = await session.exec(
47 `${buildGitCommandPrefix(hasAuthHeader)} -C ${quoteShellArg(repoRoot)} fetch${depthArgs} origin ${quoteShellArg(ref)}`,
48 {
49 cwd: repoRoot,
50 },
51 );
52
53 if (!result.success && depth !== undefined && isShallowUnsupportedError(result.stderr)) {
54 result = await session.exec(
55 `${buildGitCommandPrefix(hasAuthHeader)} -C ${quoteShellArg(repoRoot)} fetch origin ${quoteShellArg(ref)}`,
56 {
57 cwd: repoRoot,
58 },
59 );
60 }
61
62 if (!result.success) {
63 throw new Error(result.stderr || errorMessage || `Failed to fetch ${ref}.`);
64 }
65};
66
67export const initializeRepository = async (session: ExecutionSession, repoRoot: string): Promise<void> => {
68 const result = await session.exec(`git init ${quoteShellArg(repoRoot)}`, {
69 cwd: "/workspace",
70 });
71
72 if (!result.success) {
73 throw new Error(result.stderr || `Failed to initialize ${repoRoot}.`);
74 }
75};
76
77export const addOriginRemote = async (session: ExecutionSession, repoRoot: string, repoUrl: string): Promise<void> => {
78 const result = await session.exec(`git -C ${quoteShellArg(repoRoot)} remote add origin ${quoteShellArg(repoUrl)}`, {
79 cwd: repoRoot,
80 });
81
82 if (!result.success) {
83 throw new Error(result.stderr || `Failed to add origin remote for ${repoRoot}.`);
84 }
85};
86
87export const resolveCheckedOutCommitSha = async (
88 session: ExecutionSession,
89 repoRoot: string,
90): Promise<CommitShaType> => {
91 const result = await session.exec(`git -C ${quoteShellArg(repoRoot)} rev-parse HEAD`, {
92 cwd: repoRoot,
93 });
94
95 if (!result.success) {
96 throw new Error(result.stderr || "Failed to resolve checked-out commit SHA.");
97 }
98
99 try {
100 return CommitSha.assertDecode(result.stdout.trim());
101 } catch {
102 throw new Error(`Resolved commit SHA was invalid: ${result.stdout.trim()}`);
103 }
104};
105
106export const resolveShallowRepositoryState = async (session: ExecutionSession, repoRoot: string): Promise<boolean> => {
107 const result = await session.exec(`git -C ${quoteShellArg(repoRoot)} rev-parse --is-shallow-repository`, {
108 cwd: repoRoot,
109 });
110
111 if (!result.success) {
112 throw new Error(result.stderr || "Failed to resolve shallow repository state.");
113 }
114
115 const normalizedOutput = result.stdout.trim().toLowerCase();
116 if (normalizedOutput === "true") {
117 return true;
118 }
119 if (normalizedOutput === "false") {
120 return false;
121 }
122
123 throw new Error(`Unexpected shallow repository state: ${result.stdout.trim()}`);
124};
125
126export const fetchPinnedCommitBySha = async (
127 session: ExecutionSession,
128 repoRoot: string,
129 commitSha: CommitShaType,
130 hasAuthHeader: boolean,
131 depth?: number,
132 errorMessage?: string,
133): Promise<void> => {
134 await fetchRef(session, repoRoot, commitSha, hasAuthHeader, depth, errorMessage);
135};
136
137export const fetchBranch = async (
138 session: ExecutionSession,
139 repoRoot: string,
140 branch: string,
141 hasAuthHeader: boolean,
142 depth?: number,
143 errorMessage?: string,
144): Promise<void> => {
145 await fetchRef(session, repoRoot, branch, hasAuthHeader, depth, errorMessage);
146};
147
148export const unshallowRepository = async (
149 session: ExecutionSession,
150 repoRoot: string,
151 hasAuthHeader: boolean,
152): Promise<void> => {
153 const fetchResult = await session.exec(
154 `${buildGitCommandPrefix(hasAuthHeader)} -C ${quoteShellArg(repoRoot)} fetch --unshallow --update-shallow origin`,
155 {
156 cwd: repoRoot,
157 },
158 );
159
160 if (!fetchResult.success) {
161 throw new Error(fetchResult.stderr || "Failed to unshallow pinned checkout history.");
162 }
163};
164
165export const checkoutFetchedBranch = async (
166 session: ExecutionSession,
167 repoRoot: string,
168 branch: string,
169): Promise<void> => {
170 const checkoutResult = await session.exec(
171 `git -C ${quoteShellArg(repoRoot)} checkout -B ${quoteShellArg(branch)} FETCH_HEAD`,
172 {
173 cwd: repoRoot,
174 },
175 );
176 if (!checkoutResult.success) {
177 throw new Error(checkoutResult.stderr || `Failed to check out branch ${branch}.`);
178 }
179};
180
181export const setFetchedBranchUpstream = async (
182 session: ExecutionSession,
183 repoRoot: string,
184 branch: string,
185): Promise<void> => {
186 const upstreamRef = `origin/${branch}`;
187 const upstreamResult = await session.exec(
188 `git -C ${quoteShellArg(repoRoot)} branch --set-upstream-to=${quoteShellArg(upstreamRef)} ${quoteShellArg(branch)}`,
189 {
190 cwd: repoRoot,
191 },
192 );
193 if (!upstreamResult.success) {
194 throw new Error(upstreamResult.stderr || `Failed to set upstream for branch ${branch}.`);
195 }
196};
197
198export const checkoutPinnedCommit = async (
199 session: ExecutionSession,
200 repoRoot: string,
201 commitSha: CommitShaType,
202): Promise<void> => {
203 const checkoutResult = await session.exec(
204 `git -C ${quoteShellArg(repoRoot)} checkout --detach ${quoteShellArg(commitSha)}`,
205 {
206 cwd: repoRoot,
207 },
208 );
209 if (!checkoutResult.success) {
210 throw new Error(checkoutResult.stderr || `Failed to check out pinned commit ${commitSha}.`);
211 }
212};
213
214export { hasCommitObject };
215