Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

207 lines · 9.2 KBRaw History
1import { existsSync } from "node:fs";
2import { mkdir, readdir, rm } from "node:fs/promises";
3import path from "node:path";
4import { failure } from "../job.js";
5import { truthyInput } from "../util.js";
6import type { Builtin, BuiltinContext } from "./index.js";
7
8export function basicAuthHeader(token: string): string {
9 return `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`;
10}
11
12/** Derives the clone URL of `owner/repo` from the job's own clone URL. */
13export function cloneUrlFor(spec: { repo: { owner: string; name: string; cloneUrl: string } }, repository: string): string {
14 const own = `${spec.repo.owner}/${spec.repo.name}`;
15 if (repository.toLowerCase() === own.toLowerCase()) return spec.repo.cloneUrl;
16 const u = spec.repo.cloneUrl;
17 const idx = u.toLowerCase().lastIndexOf(own.toLowerCase());
18 if (idx >= 0) return u.slice(0, idx) + repository + u.slice(idx + own.length);
19 try {
20 const parsed = new URL(u);
21 return `${parsed.origin}/${repository}.git`;
22 } catch {
23 return u;
24 }
25}
26
27function isHttp(url: string): boolean {
28 return /^https?:\/\//i.test(url);
29}
30
31export const checkout: Builtin = async (c) => {
32 const spec = c.job.spec;
33 const ownRepo = `${spec.repo.owner}/${spec.repo.name}`;
34 const repository = c.input("repository", ownRepo);
35 const isOwn = repository.toLowerCase() === ownRepo.toLowerCase();
36 const token = c.input("token", spec.token);
37 const refInput = c.input("ref");
38 const depthRaw = Number(c.input("fetch-depth", "1"));
39 const fetchDepth = Number.isFinite(depthRaw) && depthRaw >= 0 ? Math.floor(depthRaw) : 1;
40 const dest = path.resolve(c.job.workspace, c.input("path", "."));
41 const clean = truthyInput(c.input("clean"), true);
42 const submodules = c.input("submodules", "false").toLowerCase();
43 const persist = truthyInput(c.input("persist-credentials"), true);
44 const fetchTags = truthyInput(c.input("fetch-tags"), false);
45 if (truthyInput(c.input("lfs"), false)) c.warn("Git LFS is not supported by the Diffinery checkout built-in; LFS pointers will not be resolved.");
46 if (c.input("sparse-checkout")) c.warn("sparse-checkout is not supported by the Diffinery checkout built-in; doing a full checkout.");
47
48 const url = cloneUrlFor(spec, repository);
49 const header = basicAuthHeader(token);
50 c.job.masker.add(token);
51 c.job.masker.add(header.slice("AUTHORIZATION: basic ".length));
52 const authArgs = isHttp(url) && token ? ["-c", `http.extraheader=${header}`] : [];
53 const env = c.toolEnv({ GIT_TERMINAL_PROMPT: "0", GIT_LFS_SKIP_SMUDGE: "1" });
54 const git = (args: string[], o: { silent?: boolean; allowFail?: boolean } = {}) =>
55 o.allowFail ? c.exec("git", args, { cwd: dest, env, capture: true, silent: o.silent }) : c.run("git", args, { cwd: dest, env, silent: o.silent });
56
57 c.log(`Syncing repository: ${repository}`);
58 await mkdir(dest, { recursive: true });
59 const hasGit = existsSync(path.join(dest, ".git"));
60 if (hasGit && clean) {
61 await git(["clean", "-ffdx"], { allowFail: true });
62 await git(["reset", "--hard", "HEAD"], { allowFail: true, silent: true });
63 } else if (!hasGit) {
64 const entries = await readdir(dest);
65 if (entries.length && clean) {
66 c.log(`Deleting the contents of '${dest}'`);
67 await Promise.all(entries.map((e) => rm(path.join(dest, e), { recursive: true, force: true })));
68 }
69 }
70 if (!hasGit) await git(["init", "-q", dest]);
71 const setUrl = await git(["remote", "set-url", "origin", url], { allowFail: true, silent: true });
72 if (setUrl.exitCode !== 0) await git(["remote", "add", "origin", url]);
73 await git(["config", "--local", "gc.auto", "0"]);
74
75 // Decide what to fetch
76 let sha: string | undefined;
77 let ref: string | undefined;
78 if (refInput) {
79 if (/^[0-9a-f]{40}$/i.test(refInput)) sha = refInput;
80 else ref = refInput;
81 } else if (isOwn) {
82 sha = spec.sha;
83 ref = spec.ref;
84 } else {
85 ref = "HEAD";
86 }
87
88 const depthArgs = fetchDepth > 0 ? [`--depth=${fetchDepth}`] : [];
89 const tagArgs = fetchDepth === 0 || fetchTags ? [] : ["--no-tags"];
90 const fetch = (refspecs: string[], allowFail = false) =>
91 git([...authArgs, "fetch", ...tagArgs, "--prune", "--no-recurse-submodules", ...depthArgs, "origin", ...refspecs], { allowFail });
92
93 let checkoutTarget: string | undefined;
94 let branch: string | undefined;
95 if (ref?.startsWith("refs/heads/")) branch = ref.slice("refs/heads/".length);
96
97 if (fetchDepth === 0) {
98 c.log("Fetching the repository (full history)");
99 await fetch(["+refs/heads/*:refs/remotes/origin/*", "+refs/tags/*:refs/tags/*"]);
100 if (ref && !ref.startsWith("refs/heads/") && !ref.startsWith("refs/tags/") && ref !== "HEAD") {
101 if (ref.startsWith("refs/")) await fetch([`+${ref}:${ref.replace(/^refs\//, "refs/remotes/")}`], true);
102 }
103 checkoutTarget = sha ?? (await resolveRef(git, ref!, (b) => (branch = b)));
104 } else {
105 c.log(`Fetching the repository (depth ${fetchDepth})`);
106 let fetched = false;
107 if (sha) {
108 const r = await fetch([`+${sha}:refs/remotes/origin/${branch ?? "diffinery-checkout"}`], true);
109 fetched = r.exitCode === 0;
110 if (!fetched) c.log("Fetching by commit SHA failed; falling back to fetching the ref.");
111 }
112 if (!fetched && ref) {
113 const specs = refspecsFor(ref);
114 let ok = false;
115 for (const rs of specs) {
116 const r = await fetch([rs.refspec], true);
117 if (r.exitCode === 0) {
118 ok = true;
119 if (rs.branch) branch = rs.branch;
120 break;
121 }
122 }
123 if (!ok) throw failure(`Failed to fetch ref '${ref}' from ${repository}. Check that the ref exists and the token has read access.`);
124 if (!sha) {
125 const target = specs.find((s) => s.branch === branch)?.local ?? "FETCH_HEAD";
126 checkoutTarget = target;
127 }
128 } else if (!fetched) {
129 throw failure(`Failed to fetch commit ${sha} from ${repository}.`);
130 }
131 checkoutTarget ??= sha ?? "FETCH_HEAD";
132 }
133
134 if (branch) await git(["checkout", "--progress", "--force", "-B", branch, checkoutTarget!]);
135 else await git(["checkout", "--progress", "--force", checkoutTarget!]);
136 const head = (await git(["rev-parse", "HEAD"], { silent: true })).stdout.trim();
137 if (sha && /^[0-9a-f]{40}$/i.test(sha) && head.toLowerCase() !== sha.toLowerCase()) {
138 throw failure(`Checked out ${head} but expected ${sha}.`);
139 }
140 c.log(`HEAD is now at ${head}`);
141 c.setOutput("commit", head);
142 c.setOutput("ref", ref && ref !== "HEAD" ? ref : branch ? `refs/heads/${branch}` : "");
143
144 if (submodules === "true" || submodules === "recursive") {
145 c.log("Fetching submodules");
146 await git(["submodule", "sync", ...(submodules === "recursive" ? ["--recursive"] : [])], { allowFail: true });
147 const r = await git(
148 [...authArgs, "submodule", "update", "--init", "--force", ...(fetchDepth > 0 ? [`--depth=${fetchDepth}`] : []), ...(submodules === "recursive" ? ["--recursive"] : [])],
149 { allowFail: true },
150 );
151 if (r.exitCode !== 0) c.warn("Failed to update submodules (best effort).");
152 }
153
154 if (persist && authArgs.length) {
155 const key = `http.${new URL(url).origin}/.extraheader`;
156 await git(["config", "--local", key, header], { silent: true });
157 c.registerPost(`Post ${c.name}`, "always()", async (pc: BuiltinContext) => {
158 if (!existsSync(path.join(dest, ".git"))) return;
159 pc.log("Removing persisted credentials");
160 await pc.exec("git", ["config", "--local", "--unset-all", key], { cwd: dest, capture: true, silent: true });
161 });
162 }
163};
164
165function refspecsFor(ref: string): Array<{ refspec: string; local: string; branch?: string }> {
166 if (ref === "HEAD") return [{ refspec: "HEAD", local: "FETCH_HEAD" }];
167 if (ref.startsWith("refs/heads/")) {
168 const b = ref.slice(11);
169 return [{ refspec: `+${ref}:refs/remotes/origin/${b}`, local: `refs/remotes/origin/${b}`, branch: b }];
170 }
171 if (ref.startsWith("refs/tags/")) return [{ refspec: `+${ref}:${ref}`, local: ref }];
172 if (ref.startsWith("refs/")) {
173 const local = ref.replace(/^refs\//, "refs/remotes/");
174 return [{ refspec: `+${ref}:${local}`, local }];
175 }
176 return [
177 { refspec: `+refs/heads/${ref}:refs/remotes/origin/${ref}`, local: `refs/remotes/origin/${ref}`, branch: ref },
178 { refspec: `+refs/tags/${ref}:refs/tags/${ref}`, local: `refs/tags/${ref}` },
179 ];
180}
181
182async function resolveRef(
183 git: (args: string[], o?: { silent?: boolean; allowFail?: boolean }) => Promise<{ exitCode: number; stdout: string }>,
184 ref: string,
185 setBranch: (b: string) => void,
186): Promise<string> {
187 if (ref === "HEAD") {
188 const r = await git(["symbolic-ref", "refs/remotes/origin/HEAD"], { allowFail: true, silent: true });
189 if (r.exitCode === 0) return r.stdout.trim();
190 }
191 const short = ref.replace(/^refs\/heads\//, "");
192 const candidates = ref.startsWith("refs/tags/")
193 ? [ref]
194 : ref.startsWith("refs/heads/")
195 ? [`refs/remotes/origin/${short}`]
196 : ref.startsWith("refs/")
197 ? [ref.replace(/^refs\//, "refs/remotes/")]
198 : [`refs/remotes/origin/${ref}`, `refs/tags/${ref}`];
199 for (const cand of candidates) {
200 const r = await git(["rev-parse", "--verify", "--quiet", cand], { allowFail: true, silent: true });
201 if (r.exitCode === 0) {
202 if (cand.startsWith("refs/remotes/origin/")) setBranch(cand.slice("refs/remotes/origin/".length));
203 return cand;
204 }
205 }
206 throw failure(`A branch or tag with the name '${ref}' could not be found`);
207}