import { createServerFn } from "@tanstack/react-start"; import { limitHeavy, limitWrite } from "~/server/limits"; import { z } from "zod"; import { requireViewer, viewerMiddleware } from "./middleware"; import { first } from "~/server/db"; import { loadRepo, repoPermission, requireRepoAccess } from "~/server/repos"; import { branchSha, listRefs, resolveRevision } from "~/server/git/refs"; import { withStore } from "~/server/git/store"; import { createBranch as pushBranch, deleteRef, isValidRefName } from "~/server/git/writer"; import { diffTreesCached } from "~/server/git/diff"; import { compareCommits } from "~/server/git/history"; 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"; import type { RepoRow, Viewer } from "~/lib/types"; 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); 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, }); }); async function resolveHead(base: RepoRow, head: string, viewer: Viewer | null) { const separator = head.indexOf(":"); if (separator === -1) return { repo: base, ref: head }; const owner = head.slice(0, separator); const ref = head.slice(separator + 1); const forks = await first<{ name: string }>( `SELECT r.name FROM repos r JOIN accounts a ON a.id = r.owner_id WHERE a.name = ? AND (r.id = ? OR r.fork_of_id = ? OR r.id = ?)`, owner, base.id, base.id, base.fork_of_id ?? "", ); if (!forks) throw notFoundError("No fork found for that owner"); const repo = await loadRepo(owner, forks.name); if (!repo || (await repoPermission(repo, viewer)) === "none") throw notFoundError("Head repository not found"); return { repo, ref }; } 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 head = await resolveHead(repo, data.head, context.viewer); const [baseRefs, headRefs] = await Promise.all([ listRefs(repo), head.repo.id === repo.id ? null : listRefs(head.repo), ]); const baseRevision = resolveRevision(baseRefs, data.base); const headRevision = resolveRevision(headRefs ?? baseRefs, head.ref); if (!baseRevision || !headRevision) throw notFoundError("Unknown branch"); const result = await withStore(repo.artifacts_name, (baseStore) => withStore(head.repo.artifacts_name, async (headStore) => { const comparison = await compareCommits(baseStore, baseRevision.sha, headStore, headRevision.sha); const [mergeBase, headCommit] = await Promise.all([ comparison.mergeBase ? headStore.commit(comparison.mergeBase) : null, headStore.commit(headRevision.sha), ]); if (!headCommit) throw notFoundError("Head commit not found"); const diff = await diffTreesCached(headStore, mergeBase?.treeSha ?? null, headCommit.treeSha); return { comparison, diff }; }), ); const existing = await 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, head.repo.id, head.ref, baseRevision.name, ); return { base: baseRevision, head: { ...headRevision, owner: head.repo.owner_name, repo: head.repo.name }, commits: result.comparison.commits, behindBy: result.comparison.behindBy, mergeBase: result.comparison.mergeBase, diff: result.diff, existingPull: existing?.number ?? null, branches: baseRefs.branches.map((branch) => branch.name), headBranches: (headRefs ?? baseRefs).branches.map((branch) => branch.name), }; });