g1t/services/repos/src/index.ts

312 lines9,449 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Initial g1t: services, event bus, intents and attempts1import { WorkerEntrypoint } from "cloudflare:workers";
2
3import {
4 type BlobView,
5 type Commit,
6 type CreateRepoInput,
7 type EventsApi,
8 type GitAccess,
9 type GitService,
API and MCP server, Rust identity service, registration, site redesign10 type ServiceBinding,
Initial g1t: services, event bus, intents and attempts11 type NewEvent,
12 type Repo,
13 type RepoPath,
14 type ReposApi,
15 type Result,
16 type TreeView,
17 type User,
18 type Viewer,
Email verification, password reset, and Git for AI scale positioning19 UNVERIFIED,
Initial g1t: services, event bus, intents and attempts20 fail,
API and MCP server, Rust identity service, registration, site redesign21 identityClient,
Initial g1t: services, event bus, intents and attempts22 isValidNamespace,
23 isValidRepoName,
24 newId,
25 ok,
26} from "@g1t/contracts";
27
28import { ArtifactsGitStore } from "./artifacts-git-store";
29import { handleGitHttp } from "./git-http";
30import type { GitStore } from "./git-store";
31import {
32 RepoRegistry,
33 canRead,
34 canWrite,
35 storeKey,
36} from "./registry";
37
38export interface ReposEnv {
39 DB: D1Database;
40 ARTIFACTS: Artifacts;
API and MCP server, Rust identity service, registration, site redesign41 IDENTITY: ServiceBinding;
Initial g1t: services, event bus, intents and attempts42 EVENTS: EventsApi;
43}
44
45/** Namespace that holds every attempt's fork: `attempts/<attempt id>`. */
46const ATTEMPTS_NAMESPACE = "attempts";
47const MAX_TEXT_BYTES = 512 * 1024;
48const README = /^readme(\.(md|markdown|txt))?$/i;
49const SOURCE = "repos";
50
51const NOT_FOUND = fail("not_found", "Repository not found.");
52
53/** Decoded text, or null when the blob is too large or looks binary. */
54async function blobText(blob: Blob): Promise<string | null> {
55 if (blob.size > MAX_TEXT_BYTES) return null;
56 const bytes = new Uint8Array(await blob.arrayBuffer());
57 if (bytes.includes(0)) return null;
58 return new TextDecoder().decode(bytes);
59}
60
61export default class ReposService
62 extends WorkerEntrypoint<ReposEnv>
63 implements ReposApi
64{
65 private readonly registry = new RepoRegistry(this.env.DB);
66 private readonly store: GitStore = new ArtifactsGitStore(this.env.ARTIFACTS);
67
68 /** Resolves a repo the viewer may read; private repos look missing. */
69 private async readable(path: RepoPath, viewer: Viewer): Promise<Repo | null> {
70 const repo = await this.registry.byPath(path);
71 return repo && canRead(repo, viewer) ? repo : null;
72 }
73
74 async get(path: RepoPath, viewer: Viewer): Promise<Result<Repo>> {
75 const repo = await this.readable(path, viewer);
76 return repo ? ok(repo) : NOT_FOUND;
77 }
78
79 async getById(id: string, viewer: Viewer): Promise<Result<Repo>> {
80 const repo = await this.registry.byId(id);
81 return repo && canRead(repo, viewer) ? ok(repo) : NOT_FOUND;
82 }
83
84 async list(
85 viewer: Viewer,
86 options: { query?: string; namespace?: string } = {},
87 ): Promise<Repo[]> {
88 return this.registry.list(viewer, options);
89 }
90
91 async create(owner: User, input: CreateRepoInput): Promise<Result<Repo>> {
Email verification, password reset, and Git for AI scale positioning92 if (!owner.verified) return UNVERIFIED;
Initial g1t: services, event bus, intents and attempts93 const name = input.name.trim().toLowerCase();
94 if (!isValidRepoName(name)) {
95 return fail("invalid", "Use letters, digits, dots, hyphens and underscores only.");
96 }
97 if (!isValidNamespace(owner.username)) {
98 return fail("invalid", "This account cannot own repositories.");
99 }
100 const path = { namespace: owner.username, name };
101 if (await this.registry.byPath(path)) {
102 return fail("conflict", "You already have a repository with that name.");
103 }
104 const repo: Repo = {
105 id: newId("rep"),
106 ...path,
107 description: input.description?.trim() || null,
108 isPrivate: input.isPrivate ?? false,
109 ownerId: owner.id,
110 defaultBranch: "main",
111 forkOf: null,
112 createdAt: Date.now(),
113 };
114 await this.store.create(storeKey(repo), {
115 description: repo.description ?? undefined,
116 defaultBranch: repo.defaultBranch,
117 });
118 await this.registry.insert(repo);
119 await this.publish({
120 type: "repo.created",
121 source: SOURCE,
122 repoId: repo.id,
123 actor: owner.id,
124 data: {
125 repoId: repo.id,
126 namespace: repo.namespace,
127 name: repo.name,
128 isPrivate: repo.isPrivate,
129 },
130 });
131 return ok(repo);
132 }
133
134 async tree(
135 path: RepoPath,
136 viewer: Viewer,
137 ref: string | null,
138 treePath: string,
139 ): Promise<Result<TreeView>> {
140 const repo = await this.readable(path, viewer);
141 if (!repo) return NOT_FOUND;
142 const key = storeKey(repo);
143 const resolvedRef = ref ?? repo.defaultBranch;
144 const base = { repo, ref: resolvedRef, path: treePath };
145
146 const [head] = await this.store.log(key, resolvedRef, 1);
147 if (!head) {
148 // An unknown ref is an error; a repo with no commits is just empty.
149 if (ref) return fail("not_found", "No such branch, tag or commit.");
150 return ok({ ...base, head: null, entries: [], readme: null });
151 }
152
153 let entries = await this.store.readTree(key, head.treeHash);
154 for (const segment of treePath.split("/").filter(Boolean)) {
155 const next = entries?.find(
156 (entry) => entry.name === segment && entry.kind === "tree",
157 );
158 if (!next) return fail("not_found", "No such directory.");
159 entries = await this.store.readTree(key, next.hash);
160 }
161 if (!entries) return fail("not_found", "No such directory.");
162 entries.sort(
163 (a, b) =>
164 Number(b.kind === "tree") - Number(a.kind === "tree") ||
165 a.name.localeCompare(b.name),
166 );
167
168 const readmeEntry = entries.find(
169 (entry) => entry.kind === "blob" && README.test(entry.name),
170 );
171 const readmeBlob = readmeEntry
172 ? await this.store.readBlob(key, readmeEntry.hash)
173 : null;
174 const readme =
175 readmeEntry && readmeBlob
176 ? { name: readmeEntry.name, text: await blobText(readmeBlob) }
177 : null;
178 return ok({ ...base, head, entries, readme });
179 }
180
181 async blob(
182 path: RepoPath,
183 viewer: Viewer,
184 ref: string,
185 filePath: string,
186 ): Promise<Result<BlobView>> {
187 const repo = await this.readable(path, viewer);
188 if (!repo) return NOT_FOUND;
189 const blob = filePath
190 ? await this.store.readFile(storeKey(repo), ref, filePath)
191 : null;
192 if (!blob) return fail("not_found", "No such file.");
193 return ok({
194 repo,
195 ref,
196 path: filePath,
197 size: blob.size,
198 text: await blobText(blob),
199 });
200 }
201
202 async log(
203 path: RepoPath,
204 viewer: Viewer,
205 ref: string | null,
206 limit: number,
207 ): Promise<Result<Commit[]>> {
208 const repo = await this.readable(path, viewer);
209 if (!repo) return NOT_FOUND;
210 return ok(
211 await this.store.log(storeKey(repo), ref ?? repo.defaultBranch, limit),
212 );
213 }
214
215 async forkForAttempt(
216 sourceId: string,
217 attemptId: string,
218 actor: User,
219 ): Promise<Result<Repo>> {
220 const source = await this.registry.byId(sourceId);
221 if (!source || !canRead(source, actor)) return NOT_FOUND;
222 const fork: Repo = {
223 id: newId("rep"),
224 namespace: ATTEMPTS_NAMESPACE,
225 name: attemptId,
226 description: null,
227 // A fork is exactly as visible as the repo it came from.
228 isPrivate: source.isPrivate,
229 ownerId: actor.id,
230 defaultBranch: source.defaultBranch,
231 forkOf: source.id,
232 createdAt: Date.now(),
233 };
234 await this.store.fork(storeKey(source), storeKey(fork));
235 await this.registry.insert(fork);
236 await this.publish({
237 type: "repo.forked",
238 source: SOURCE,
239 repoId: source.id,
240 actor: actor.id,
241 data: { repoId: fork.id, sourceRepoId: source.id, attemptId },
242 });
243 return ok(fork);
244 }
245
246 async gitAccess(
247 path: RepoPath,
248 viewer: Viewer,
249 service: GitService,
250 ): Promise<Result<GitAccess>> {
251 const write = service === "git-receive-pack";
Email verification, password reset, and Git for AI scale positioning252 if (write && viewer && !viewer.verified) return UNVERIFIED;
Initial g1t: services, event bus, intents and attempts253 let repo = await this.registry.byPath(path);
254 if (!repo) {
255 // Push to create, in the pusher's own namespace only.
256 if (!write || !viewer || viewer.username !== path.namespace.toLowerCase()) {
257 return this.denied(viewer);
258 }
259 const created = await this.create(viewer, { name: path.name });
260 if (!created.ok) return created;
261 repo = created.value;
262 } else if (write ? !canWrite(repo, viewer) : !canRead(repo, viewer)) {
263 return this.denied(viewer);
264 }
265 return ok(await this.store.access(storeKey(repo), write ? "write" : "read"));
266 }
267
268 /**
269 * Anonymous callers are asked to authenticate whether or not the repo
270 * exists, so private repos cannot be told apart from missing ones.
271 */
272 private denied(viewer: Viewer): Result<never> {
273 return viewer
274 ? NOT_FOUND
275 : fail("unauthenticated", "Authentication required.");
276 }
277
278 private async publish(event: NewEvent): Promise<void> {
279 await this.env.EVENTS.publish([event]);
280 }
281
282 /** Git over HTTPS. */
283 async fetch(request: Request): Promise<Response> {
API and MCP server, Rust identity service, registration, site redesign284 const response = await handleGitHttp(request, identityClient(this.env.IDENTITY), this, (path) =>
Initial g1t: services, event bus, intents and attempts285 this.ctx.waitUntil(this.pushed(path)),
286 );
287 return response ?? new Response("Not found\n", { status: 404 });
288 }
289
290 /**
291 * Publishes `git.push` after a push has gone through the git front end.
292 * Artifacts' own push subscriptions are per repository, which does not fit
293 * a repo per attempt, so the front end reports pushes itself.
294 */
295 private async pushed(path: RepoPath): Promise<void> {
296 const repo = await this.registry.byPath(path);
297 if (!repo) return;
298 const [head] = await this.store.log(storeKey(repo), repo.defaultBranch, 1);
299 if (!head) return;
300 await this.publish({
301 type: "git.push",
302 source: SOURCE,
303 repoId: repo.id,
304 actor: null,
305 data: {
306 repoId: repo.id,
307 ref: `refs/heads/${repo.defaultBranch}`,
308 after: head.hash,
309 },
310 });
311 }
312}