Vectrum
Sign In
JjohannSoAdd wikisHistory
114 lines · 4.4 KB · TypeScript
import { randomBytes } from "node:crypto";
import { rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { git, GitError } from "./git";

export type FileChange =
  | { path: string; content: Buffer } // create or overwrite
  | { path: string; delete: true };

const ZERO_SHA = "0".repeat(40);

/** Validates a repository-relative file path typed by a user. */
export function cleanRepoPath(input: string) {
  const parts = input.trim().replace(/^\/+|\/+$/g, "").split("/");
  if (parts.length === 0 || parts.some((p) => !p || p === "." || p === ".." || p.toLowerCase() === ".git")) {
    return null;
  }
  const clean = parts.join("/");
  return clean.length <= 1024 && !/[\0\n\r]/.test(clean) ? clean : null;
}

/**
 * Creates a commit on `branch` directly in a bare repository using a throwaway
 * index, without needing a working tree. `parent` is the commit the changes are
 * based on (null for the first commit or a new orphan branch). The branch ref
 * is updated atomically, so a concurrent push makes this fail instead of being
 * overwritten.
 */
export async function commitFiles(
  dir: string,
  opts: {
    branch: string;
    parent: string | null;
    /** Expected current value of the branch; defaults to `parent`. Use null to require a new branch. */
    expectedHead?: string | null;
    changes: FileChange[];
    message: string;
    author: { name: string; email: string };
  },
) {
  const indexFile = path.join(os.tmpdir(), `mng-index-${randomBytes(8).toString("hex")}`);
  const env = {
    GIT_INDEX_FILE: indexFile,
    GIT_AUTHOR_NAME: opts.author.name,
    GIT_AUTHOR_EMAIL: opts.author.email,
    GIT_COMMITTER_NAME: opts.author.name,
    GIT_COMMITTER_EMAIL: opts.author.email,
  };
  try {
    if (opts.parent) await git(dir, ["read-tree", "--end-of-options", opts.parent], { env });
    else await git(dir, ["read-tree", "--empty"], { env });

    for (const change of opts.changes) {
      if ("delete" in change) {
        // Mode 0 removes the entry; unlike --force-remove this works without a work tree.
        await git(dir, ["update-index", "--index-info"], { env, input: `0 ${ZERO_SHA}\t${change.path}\n` });
        continue;
      }
      const sha = (await git(dir, ["hash-object", "-w", "--stdin"], { input: change.content })).trim();
      // Keep the executable bit if the file already exists with it.
      const existing = await git(dir, ["ls-files", "-s", "--", change.path], { env });
      const mode = existing.startsWith("100755") ? "100755" : "100644";
      await git(dir, ["update-index", "--add", "--cacheinfo", `${mode},${sha},${change.path}`], { env });
    }

    const tree = (await git(dir, ["write-tree"], { env })).trim();
    if (opts.parent) {
      const parentTree = (await git(dir, ["rev-parse", "--verify", "--end-of-options", `${opts.parent}^{tree}`])).trim();
      if (parentTree === tree) throw new GitError("No changes to commit.");
    }
    const args = ["commit-tree", tree];
    if (opts.parent) args.push("-p", opts.parent);
    const commit = (await git(dir, args, { env, input: opts.message })).trim();

    const expected = opts.expectedHead === undefined ? opts.parent : opts.expectedHead;
    try {
      await git(dir, ["update-ref", "-m", "web edit", `refs/heads/${opts.branch}`, commit, expected ?? ZERO_SHA]);
    } catch {
      throw new GitError(
        expected ? `Branch ${opts.branch} was updated by someone else. Reload and try again.` : `Branch ${opts.branch} already exists.`,
      );
    }
    return commit;
  } finally {
    await rm(indexFile, { force: true });
  }
}

/** A tag name git accepts, without path components starting with "-" (pushes of those are refused too). */
export async function isValidTagName(dir: string, name: string) {
  if (!name || name.length > 255 || name.split("/").some((c) => c.startsWith("-"))) return false;
  return git(dir, ["check-ref-format", `refs/tags/${name}`]).then(
    () => true,
    () => false,
  );
}

/** Creates a lightweight tag at a commit, failing if the tag already exists. */
export async function createTag(dir: string, name: string, sha: string) {
  try {
    await git(dir, ["update-ref", "-m", "release", `refs/tags/${name}`, sha, ZERO_SHA]);
  } catch {
    throw new GitError(`Tag ${name} already exists.`);
  }
}

export async function isValidBranchName(dir: string, name: string) {
  if (!name || name.startsWith("-")) return false;
  return git(dir, ["check-ref-format", "--branch", name]).then(
    () => true,
    () => false,
  );
}