pr_01m47d15m3e54sn21z27rpy5n9/services/repos/src/artifacts-git-store.ts

96 lines2,811 bytesCodeBlame
1import type { Commit, GitAccess, TreeEntry } from "@g1t/contracts";
2
3import type { GitStore } from "./git-store";
4
5const TOKEN_TTL_SECONDS = 300;
6
7function isCode(error: unknown, code: ArtifactsErrorCode): boolean {
8 return (error as Partial<ArtifactsError> | null)?.code === code;
9}
10
11function toCommit(commit: ArtifactsCommitMetadata): Commit {
12 return {
13 hash: commit.hash,
14 treeHash: commit.treeHash,
15 message: commit.message,
16 author: commit.author,
17 parents: commit.parents,
18 authoredAt: commit.authoredAt * 1000,
19 };
20}
21
22/** {@link GitStore} backed by Cloudflare Artifacts. */
23export class ArtifactsGitStore implements GitStore {
24 constructor(private readonly artifacts: Artifacts) {}
25
26 private async use<T>(key: string, fn: (repo: ArtifactsRepo) => Promise<T>): Promise<T> {
27 const repo = await this.artifacts.get(key);
28 try {
29 return await fn(repo);
30 } finally {
31 repo[Symbol.dispose]();
32 }
33 }
34
35 async create(
36 key: string,
37 options: { description?: string; defaultBranch: string },
38 ): Promise<void> {
39 try {
40 await this.artifacts.create(key, {
41 description: options.description,
42 setDefaultBranch: options.defaultBranch,
43 });
44 } catch (error) {
45 // Left behind by an earlier failed attempt; adopt it.
46 if (!isCode(error, "ALREADY_EXISTS")) throw error;
47 }
48 }
49
50 async fork(sourceKey: string, targetKey: string): Promise<void> {
51 await this.use(sourceKey, async (repo) => {
52 try {
53 await repo.fork(targetKey, { defaultBranchOnly: true });
54 } catch (error) {
55 if (!isCode(error, "ALREADY_EXISTS")) throw error;
56 }
57 });
58 }
59
60 async access(key: string, scope: "read" | "write"): Promise<GitAccess> {
61 return this.use(key, async (repo) => {
62 const [info, token] = await Promise.all([
63 repo.info(),
64 repo.createToken(scope, TOKEN_TTL_SECONDS),
65 ]);
66 return { remote: info.remote, token: token.plaintext };
67 });
68 }
69
70 async log(key: string, ref: string, limit: number): Promise<Commit[]> {
71 return this.use(key, async (repo) =>
72 (await repo.log({ ref, limit })).map(toCommit),
73 );
74 }
75
76 async readTree(key: string, treeHash: string): Promise<TreeEntry[] | null> {
77 return this.use(key, async (repo) => {
78 const entries = await repo.readTree(treeHash);
79 return (
80 entries?.map((entry) => ({
81 name: entry.name,
82 hash: entry.hash,
83 kind: entry.type,
84 })) ?? null
85 );
86 });
87 }
88
89 async readBlob(key: string, blobHash: string): Promise<Blob | null> {
90 return this.use(key, (repo) => repo.readBlob(blobHash));
91 }
92
93 async readFile(key: string, ref: string, path: string): Promise<Blob | null> {
94 return this.use(key, (repo) => repo.readFile({ ref, path }));
95 }
96}