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.
*/