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 };
}