import type { Actor } from "./actor"; import { defaultBranch, git, GitError, revParse } from "./git"; import { deleteBranch, isValidBranchName, ZERO_SHA } from "./git-write"; import { assertRefUpdates } from "./protection"; import { openPullsFor, openPullsFromFork } from "./pulls"; import { afterRefUpdated } from "./push"; import { markPushed, type RepoRecord } from "./repos"; // Creating and deleting branches on their own, from the web UI and MCP, under the branch rules. /** Creates a branch at `from` (a branch, tag or commit; the default branch's head when not given). Returns its commit. */ export async function createBranch(repo: RepoRecord, actor: Actor, name: string, from?: string) { if (!(await isValidBranchName(repo.dir, name))) throw new GitError(`${name} isn't a valid branch name.`); const start = from || (await defaultBranch(repo.dir)); if (!start) throw new GitError("The repository is empty: it has no commits to start a branch from."); const sha = await revParse(repo.dir, start); if (!sha) throw new GitError(`There's no branch, tag or commit named ${start}.`); await assertRefUpdates(repo, actor, [{ ref: `refs/heads/${name}`, old: null, new: sha, fastForward: true }]); try { await git(repo.dir, ["update-ref", "-m", "create branch", `refs/heads/${name}`, sha, ZERO_SHA]); } catch { throw new GitError(`Branch ${name} already exists.`); } await markPushed(repo.id); afterRefUpdated(repo, `refs/heads/${name}`, null, sha, actor); return sha; } /** * Deletes a branch (not the default one), if it's still at `expectedSha` when that's given. Returns * the commit it was at and the open pull requests that used it. */ export async function removeBranch(repo: RepoRecord, actor: Actor, name: string, expectedSha?: string) { const sha = await revParse(repo.dir, `refs/heads/${name}`); if (!sha) throw new GitError(`There's no branch named ${name}.`); if (name === (await defaultBranch(repo.dir))) throw new GitError(`${name} is the default branch and can't be deleted.`); if (expectedSha && !sha.startsWith(expectedSha.toLowerCase())) throw new GitError(`${name} is at ${sha} now, not ${expectedSha}.`); await assertRefUpdates(repo, actor, [{ ref: `refs/heads/${name}`, old: sha, new: null, fastForward: true }]); const open = [...(await openPullsFor(repo.id, name, "head")), ...(await openPullsFor(repo.id, name, "base")), ...(await openPullsFromFork(repo.id, name))]; await deleteBranch(repo.dir, name, sha); afterRefUpdated(repo, `refs/heads/${name}`, sha, null, actor); return { sha, open }; }