72 lines · 2.4 KB
import type { GitStore } from "../git/store";
import type { CommitSummary } from "~/lib/types";
import type { IndexedCommit } from "./commit-index";

/** A push bigger than this is cheaper to absorb with a paged rebuild than a parent-by-parent walk. */
const MAX_INCREMENTAL = 1000;
const PREFETCH = 200;

const CO_AUTHOR = /^co-authored-by:\s*[^<\n]*<([^>\s]+@[^>\s]+)>\s*$/gim;
const MAX_CO_AUTHORS = 20;

/** `Co-authored-by: Name <email>` trailers, as GitHub credits them; the author is never listed twice. */
export function coAuthorEmails(message: string, authorEmail: string): string[] {
  const author = authorEmail.trim().toLowerCase();
  const emails = new Set<string>();
  for (const match of message.matchAll(CO_AUTHOR)) {
    const email = match[1].toLowerCase();
    if (email !== author) emails.add(email);
    if (emails.size >= MAX_CO_AUTHORS) break;
  }
  return [...emails];
}

export function toIndexed(
  commit: Pick<CommitSummary, "sha" | "authorName" | "authorEmail" | "authoredAt" | "message">,
): IndexedCommit {
  return {
    sha: commit.sha,
    authorName: commit.authorName,
    authorEmail: commit.authorEmail,
    authoredAt: commit.authoredAt,
    coAuthors: coAuthorEmails(commit.message, commit.authorEmail),
  };
}

/**
 * Walks parents from `head`, stopping at indexed commits. Because the index is closed under ancestry, the
 * old head is a boundary of that walk exactly when the push was a fast-forward; anything else (force-push,
 * rewind, a different default branch) returns null and needs a rebuild.
 */