Vectrum
Sign In
43 lines · 2.5 KB · TypeScript
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 };
}