import { createServerFn } from "@tanstack/react-start"; import { withAuthorAccounts } from "~/server/commit-authors"; 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 { CODE_MAX_PAGES, codeIndexStatus, contentTerms, ftsQuery, searchOwnerCode, searchRepoCode, type IndexStatus, } from "~/server/code/search"; import { ensureCodeIndex } from "~/server/code/search-index"; import { renderNotebook } from "~/server/code/notebook"; import { repoRef } from "~/lib/validation"; import type { CodeSearchPage } 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: { ...history, commits: await withAuthorAccounts(history.commits) }, 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(), page: z.number().int().min(1).max(CODE_MAX_PAGES).default(1), }), ) .handler(async ({ data, context }) => { const query = ftsQuery(data.q); const empty: CodeSearchPage = { hits: [], page: 1, hasMore: false, total: 0, totalCapped: false }; const base = { status: null as IndexStatus, indexing: false, reposSearched: 0, reposTruncated: false }; const invalid = !query && data.q.trim().length > 0; const options = { page: data.page, terms: contentTerms(data.q) }; if (data.owner && data.repo) { const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); const [pending, current] = await Promise.all([ensureCodeIndex(repo), codeIndexStatus(repo.id)]); const scope = { ...base, status: current.indexed, indexing: pending || current.indexing }; if (!query) return { ...empty, ...scope, invalid }; await limitHeavy(context.viewer); const ref = { id: repo.id, owner: repo.owner_name, name: repo.name, ref: repo.default_branch }; return { ...(await searchRepoCode(ref, query, options)), ...scope, invalid: false }; } // There is no instance-wide code index: code search runs inside one repository or across one owner. if (!data.owner || !query) return { ...empty, ...base, invalid }; await limitHeavy(context.viewer); const result = await searchOwnerCode(context.viewer, data.owner, query, options); return { ...result, status: null as IndexStatus, indexing: false, 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("/"), }); });