import { createServerFn } from "@tanstack/react-start"; import { withAuthorAccounts } from "~/server/commit-authors"; import { limitHeavy, limitWrite } from "~/server/limits"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { repoDb } from "~/server/scoped-db"; import { requireRepoAccess } from "~/server/repos"; import { branchSha, listRefs, resolveRevision } from "~/server/git/refs"; import { createBranch as pushBranch, deleteRef, isValidRefName } from "~/server/git/writer"; import { compareBranches } from "~/server/git/compare"; import { aiAccessFor } from "~/server/pull-describe/service"; import { badRequest, conflict, notFoundError } from "~/server/errors"; import { ZERO_SHA } from "~/server/git/store"; import { repoRef } from "~/lib/validation"; import { announcePush as announce, assertDirectUpdate, commitFileChange, rethrowPush } from "~/server/git/web-commit"; const MAX_EDIT_BYTES = 5 * 1024 * 1024; const encoder = new TextEncoder(); export const createBranch = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ name: z.string().min(1), from: z.string().min(1) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, viewer, "write"); if (!isValidRefName(data.name)) throw badRequest("Invalid branch name"); const refs = await listRefs(repo); if (branchSha(refs, data.name)) throw conflict("Branch already exists"); const source = resolveRevision(refs, data.from); if (!source) throw notFoundError("Source ref not found"); await assertDirectUpdate(repo, permission, data.name, false); await pushBranch(repo, data.name, source.sha).catch(rethrowPush); await announce(repo, viewer, `refs/heads/${data.name}`, ZERO_SHA, source.sha); return { ok: true }; }); export const deleteBranch = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ name: z.string().min(1) })) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, viewer, "write"); if (data.name === repo.default_branch) throw badRequest("The default branch cannot be deleted"); const sha = branchSha(await listRefs(repo), data.name); if (!sha) throw notFoundError("Branch not found"); await assertDirectUpdate(repo, permission, data.name, true); await deleteRef(repo, `refs/heads/${data.name}`, sha).catch(rethrowPush); await announce(repo, viewer, `refs/heads/${data.name}`, sha, ZERO_SHA); return { ok: true }; }); const editInput = repoRef.extend({ branch: z.string().min(1), path: z.string().min(1).max(4096), previousPath: z.string().nullable(), content: z.string().max(MAX_EDIT_BYTES).nullable(), message: z.string().trim().min(1).max(10_000), expectedHeadSha: z.string().nullable(), newBranch: z.string().nullable(), }); /** Commits a single-file create/edit/rename/delete from the web editor, optionally onto a new branch. */ export const commitFile = createServerFn({ method: "POST" }) .middleware([viewerMiddleware]) .validator(editInput) .handler(async ({ data, context }) => { const viewer = requireViewer(context.viewer); await limitWrite(viewer); const { repo, permission } = await requireRepoAccess(data.owner, data.repo, viewer, "write"); return commitFileChange({ repo, permission, viewer, branch: data.branch, path: data.path, previousPath: data.previousPath, content: data.content === null ? null : encoder.encode(data.content), message: data.message, expectedHeadSha: data.expectedHeadSha, newBranch: data.newBranch, }); }); export const compareRefs = createServerFn({ method: "GET" }) .middleware([viewerMiddleware]) .validator(repoRef.extend({ base: z.string().min(1), head: z.string().min(1) })) .handler(async ({ data, context }) => { await limitHeavy(context.viewer); const { repo } = await requireRepoAccess(data.owner, data.repo, context.viewer, "read"); const compared = await compareBranches(repo, data.base, data.head, context.viewer); const [existing, ai] = await Promise.all([ repoDb(repo.id).first<{ number: number }>( `SELECT i.number FROM pull_requests p JOIN issues i ON i.id = p.issue_id WHERE i.repo_id = ? AND p.head_repo_id = ? AND p.head_ref = ? AND p.base_ref = ? AND i.state = 'open'`, repo.id, compared.headRepo.id, compared.head.name, compared.base.name, ), context.viewer ? aiAccessFor(context.viewer.id, repo.id) : null, ]); return { base: compared.base, head: { ...compared.head, owner: compared.headRepo.owner_name, repo: compared.headRepo.name }, commits: await withAuthorAccounts(compared.comparison.commits), behindBy: compared.comparison.behindBy, mergeBase: compared.comparison.mergeBase, diff: compared.diff, existingPull: existing?.number ?? null, branches: compared.baseBranches, headBranches: compared.headBranches, ai, }; });