pr_01m47d15m3e54sn21z27rpy5n9/services/repos/src/artifacts-git-store.ts
| 1 | import type { Commit, GitAccess, TreeEntry } from "@g1t/contracts"; |
| 2 | |
| 3 | import type { GitStore } from "./git-store"; |
| 4 | |
| 5 | const TOKEN_TTL_SECONDS = 300; |
| 6 | |
| 7 | function isCode(error: unknown, code: ArtifactsErrorCode): boolean { |
| 8 | return (error as Partial<ArtifactsError> | null)?.code === code; |
| 9 | } |
| 10 | |
| 11 | function 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. */ |
| 23 | export 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 | } |