import { z } from "zod"; import { batch, first, stmt } from "../db"; import { badRequest, conflict, notFoundError } from "../errors"; import { recordActivity } from "../events/emit"; import { branchSha, listRefs, resolveRevision } from "../git/refs"; import { ZERO_SHA } from "../git/store"; import { announcePush, assertDirectUpdate, rethrowPush } from "../git/web-commit"; import { createBranch as pushBranch, deleteRef, isValidRefName } from "../git/writer"; import { assertBelowCap, CAPS, limitWrite } from "../limits"; import { repoMoveStatements } from "../redirects"; import { disableRepoConfigOnTransfer, repoOwnershipChangeStatements } from "../integrations/ownership"; import { rebillRepoLfs } from "../lfs/objects"; import { assertCanTransfer } from "../fork-policy"; import { syncHostingForRepo } from "../sites/sync"; import { noContent, ownerAccount, readJson, repoJson, requireAccountScope, requireScopedRepo } from "./http"; import type { ApiContext } from "./http"; const MAX_LISTED_REFS = 90; export async function listBranchesApi(ctx: ApiContext, owner: string, name: string): Promise { const { repo } = await requireScopedRepo(ctx, owner, name, "contents", "read"); const refs = await listRefs(repo); return Response.json({ default_branch: repo.default_branch, branches: refs.branches.slice(0, MAX_LISTED_REFS).map((branch) => ({ name: branch.name, sha: branch.sha })), }); } const branchInput = z.object({ name: z.string().min(1), from: z.string().min(1), }); /** Same steps as the createBranch server fn, including protection and push-rule checks. */ export async function createBranchApi(ctx: ApiContext, owner: string, name: string): Promise { const data = await readJson(ctx.request, branchInput); await limitWrite(ctx.viewer); const { repo, permission } = await requireScopedRepo(ctx, owner, name, "contents", "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 announcePush(repo, ctx.viewer, `refs/heads/${data.name}`, ZERO_SHA, source.sha); return Response.json({ name: data.name, sha: source.sha }, { status: 201 }); } export async function deleteBranchApi(ctx: ApiContext, owner: string, name: string, branch: string) { await limitWrite(ctx.viewer); const { repo, permission } = await requireScopedRepo(ctx, owner, name, "contents", "write"); if (branch === repo.default_branch) throw badRequest("The default branch cannot be deleted"); const sha = branchSha(await listRefs(repo), branch); if (!sha) throw notFoundError("Branch not found"); await assertDirectUpdate(repo, permission, branch, true); await deleteRef(repo, `refs/heads/${branch}`, sha).catch(rethrowPush); await announcePush(repo, ctx.viewer, `refs/heads/${branch}`, sha, ZERO_SHA); return noContent(); } export async function listTagsApi(ctx: ApiContext, owner: string, name: string): Promise { const { repo } = await requireScopedRepo(ctx, owner, name, "contents", "read"); const refs = await listRefs(repo); return Response.json(refs.tags.slice(0, MAX_LISTED_REFS).map((tag) => ({ name: tag.name, sha: tag.sha }))); } const transferInput = z.object({ new_owner: z.string().min(1).max(64) }); /** Same checks and redirect bookkeeping as the transferRepo server fn. */ export async function transferRepoApi(ctx: ApiContext, owner: string, name: string): Promise { const data = await readJson(ctx.request, transferInput); requireAccountScope(ctx); await limitWrite(ctx.viewer); const { repo } = await requireScopedRepo(ctx, owner, name, "admin", "admin"); const target = await ownerAccount(data.new_owner); if (target.id === repo.owner_id) throw badRequest("The repository already belongs to that account"); await assertCanTransfer(repo, target, ctx.viewer); if (await first("SELECT 1 AS ok FROM repos WHERE owner_id = ? AND name = ?", target.id, repo.name)) { throw conflict(`${target.name} already has a repository named ${repo.name}`); } const owned = await first<{ count: number }>("SELECT COUNT(*) AS count FROM repos WHERE owner_id = ?", target.id); assertBelowCap(owned?.count ?? 0, CAPS.reposPerOwner, "repositories per owner"); const now = Date.now(); await batch([ stmt("UPDATE repos SET owner_id = ?, updated_at = ? WHERE id = ?", target.id, now, repo.id), ...repoMoveStatements( repo.id, { owner: repo.owner_name, name: repo.name }, { owner: target.name, name: repo.name }, ), ...repoOwnershipChangeStatements(repo.id, ctx.viewer.id), ]); await disableRepoConfigOnTransfer(repo.id, repo.owner_id); await rebillRepoLfs(repo.id, target.id); await syncHostingForRepo(repo.id, repo.owner_id); await recordActivity(ctx.viewer.id, repo.id, "transfer", { from: repo.owner_name, to: target.name, }); return Response.json(await repoJson({ ...repo, owner_id: target.id, owner_name: target.name })); }