prose

Where the repository lives

The repository's name and GitHub address, read from its origin remote. Every page shows the name. The address lets a page link to the repository, a commit and a tag.

The name comes from the remote, not the folder, because a host builds in a checkout folder of its own. Cloudflare's is called repo. With no remote, or for a subfolder, the folder's name stands in. With no GitHub remote, the page leaves the links out.

import { execFileSync } from "node:child_process";
import { realpathSync } from "node:fs";
import { basename, resolve } from "node:path";

/** `git@github.com:user/repo.git`, `ssh://git@github.com/user/repo.git` and
 *  `https://github.com/user/repo.git` are all `https://github.com/user/repo`. */
export function githubUrl(remote: string): string | undefined {
  const match =
    /^(?:git@github\.com:|(?:ssh:\/\/git@|https?:\/\/(?:[^@/]+@)?)github\.com\/)([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/.exec(
      remote.trim(),
    );
  return match ? `https://github.com/${match[1]}` : undefined;
}

/** The repository's name from any remote: its last path segment, without `.git`. */
export function remoteName(remote: string): string | undefined {
  return /([^/:\s]+?)(?:\.git)?\/?$/.exec(remote.trim())?.[1];
}

const origins = new Map<string, string | undefined>();

/** The `origin` remote, asked of git once per folder. */
function origin(root: string): string | undefined {
  if (!origins.has(root)) {
    let remote: string | undefined;
    try {
      remote = execFileSync("git", ["remote", "get-url", "origin"], {
        cwd: root,
        encoding: "utf-8",
        stdio: ["ignore", "pipe", "ignore"],
      });
    } catch {
      remote = undefined;
    }
    origins.set(root, remote);
  }
  return origins.get(root);
}

/** The repository's GitHub address, if `origin` is on GitHub. */
export function repoUrl(root: string): string | undefined {
  const remote = origin(root);
  return remote ? githubUrl(remote) : undefined;
}

/** The project's name: the `origin` repository's, or the folder's when there's no remote or the
 *  project is a subfolder (`prose tests/fixtures/simple` is `simple`). */
export function projectName(root: string): string {
  const folder = basename(resolve(root));
  const remote = origin(root);
  if (!remote) return folder;
  let top: string;
  try {
    top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
      cwd: root,
      encoding: "utf-8",
      stdio: ["ignore", "pipe", "ignore"],
    }).trim();
  } catch {
    return folder;
  }
  return realpathSync(top) === realpathSync(root) ? (remoteName(remote) ?? folder) : folder;
}