import { createServerFn } from "@tanstack/react-start"; import { assertVisibleRevision } from "~/server/git/reachable"; import { z } from "zod"; import { viewerMiddleware } from "./middleware"; import { limitBrowse, limitHeavy } from "~/server/limits"; import { assertRepoReady, requireRepoAccess } from "~/server/repos"; import { listRefs, resolveRevision, splitRevisionPath } from "~/server/git/refs"; import { withStore } from "~/server/git/store"; import { HttpError, notFoundError } from "~/server/errors"; import { blameFile, BlameUnavailable } from "~/server/code/blame"; import { lastCommitsForDirectory, pathHistory } from "~/server/code/path-history"; import { listAllFiles } from "~/server/code/files"; import { codeIndexStatus, ftsQuery, searchAllCode, searchRepoCode } from "~/server/code/search"; import { ensureCodeIndex } from "~/server/code/search-index"; import { renderNotebook } from "~/server/code/notebook"; import { repoRef } from "~/lib/validation"; import type { CodeSearchHit } from "~/lib/code-intel"; import type { Viewer } from "~/lib/types"; const HTTP_UNPROCESSABLE = 422; const MAX_NOTEBOOK_BYTES = 5 * 1024 * 1024; const decoder = new TextDecoder(); const splatInput = repoRef.extend({ splat: z.string().default("") }); async function resolveSplat(owner: string, name: string, splat: string, viewer: Viewer | null) { const { repo } = await requireRepoAccess(owner, name, viewer, "read"); assertRepoReady(repo); const refs = await listRefs(repo); const resolved = splitRevisionPath(refs, splat); if (!resolved) throw notFoundError("Branch or path not found"); await assertVisibleRevision(repo, resolved.revision); return { repo, refs, ...resolved }; } export const getBlame = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(splatInput) .handler(async ({ data, context }) => { await limitHeavy(context.viewer); const { repo, refs, revision, path } = await resolveSplat(data.owner, data.repo, data.splat, context.viewer); if (!path) throw notFoundError("File not found"); return withStore(repo.artifacts_name, async (store) => { const commit = await store.commit(revision.sha); const entry = commit ? await store.entryAtPath(commit.treeSha, path) : null; if (!commit || !entry || entry.type === "tree") throw notFoundError("File not found"); const base = { ref: revision, path, branches: refs.branches.map((branch) => branch.name) }; try { const outcome = await blameFile(store, commit, path, entry.sha); return outcome.status === "ready" ? { ...base, blame: outcome.blame, pending: null, error: null } : { ...base, blame: null, pending: { versions: outcome.versions }, error: null }; } catch (error) { if (error instanceof BlameUnavailable) return { ...base, blame: null, pending: null, error: error.message }; throw error; } }); }); export const getPathHistory = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(splatInput.extend({ page: z.number().int().min(1).max(100).default(1) })) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); assertRepoReady(repo); const refs = await listRefs(repo); const resolved = data.splat ? splitRevisionPath(refs, data.splat) : null; const branches = refs.branches.map((branch) => branch.name); if (!resolved || !resolved.path) { return { ref: resolved?.revision.name ?? null, path: "", history: null, branches }; } await assertVisibleRevision(repo, resolved.revision); await limitHeavy(context.viewer); const history = await withStore(repo.artifacts_name, (store) => pathHistory(store, resolved.revision.sha, resolved.path, data.page), ); return { ref: resolved.revision.name, path: resolved.path, history, branches, }; }); export const getLastCommits = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ ref: z.string().min(1), path: z.string().default("") })) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); assertRepoReady(repo); const revision = resolveRevision(await listRefs(repo), data.ref); if (!revision) throw notFoundError("Branch not found"); await assertVisibleRevision(repo, revision); return withStore(repo.artifacts_name, (store) => lastCommitsForDirectory(store, revision.sha, data.path)); }); export const getFileList = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ ref: z.string().default("") })) .handler(async ({ data, context }) => { await limitBrowse(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); assertRepoReady(repo); const refs = await listRefs(repo); const revision = resolveRevision(refs, data.ref || repo.default_branch); if (!revision) throw notFoundError("Branch not found"); await assertVisibleRevision(repo, revision); const listing = await withStore(repo.artifacts_name, async (store) => { const commit = await store.commit(revision.sha); if (!commit) throw notFoundError("Commit not found"); return listAllFiles(store, commit.treeSha); }); return { ref: revision.name, ...listing }; }); export const searchCode = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator( z.object({ q: z.string().max(500), owner: z.string().min(1).optional(), repo: z.string().min(1).optional() }), ) .handler(async ({ data, context }) => { const query = ftsQuery(data.q); const scoped = data.owner && data.repo ? { owner: data.owner, repo: data.repo } : null; const access = scoped ? await requireRepoAccess(scoped.owner, scoped.repo, context.viewer, "read") : null; const status = access ? await codeIndexStatus(access.repo.id) : null; const indexing = access ? await ensureCodeIndex(access.repo, status?.commitSha ?? null) : false; if (!query) return { hits: [] as CodeSearchHit[], status, indexing, invalid: data.q.trim().length > 0 }; await limitHeavy(context.viewer); const hits = access ? await searchRepoCode(access.repo.id, query) : await searchAllCode(context.viewer, query); return { hits, status, indexing, invalid: false }; }); export const getNotebook = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(splatInput) .handler(async ({ data, context }) => { await limitHeavy(context.viewer); const { repo, revision, path } = await resolveSplat(data.owner, data.repo, data.splat, context.viewer); if (!path.endsWith(".ipynb")) throw notFoundError("Notebook not found"); const bytes = await withStore(repo.artifacts_name, async (store) => { const commit = await store.commit(revision.sha); const entry = commit ? await store.entryAtPath(commit.treeSha, path) : null; return entry && entry.type !== "tree" ? store.blob(entry.sha) : null; }); if (!bytes) throw notFoundError("Notebook not found"); if (bytes.byteLength > MAX_NOTEBOOK_BYTES) throw new HttpError(HTTP_UNPROCESSABLE, "Notebook is too large to render"); return renderNotebook(decoder.decode(bytes), { owner: repo.owner_name, repo: repo.name, ref: revision.name, basePath: path.split("/").slice(0, -1).join("/"), }); });