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 ` 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(); 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, ): 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. */ export async function fastForward( store: Pick, indexed: (sha: string) => boolean, oldHead: string, head: string, ): Promise { if (indexed(head)) return null; await store.prefetchHistory(head, PREFETCH); const fresh: IndexedCommit[] = []; const seen = new Set([head]); const queue = [head]; let reachedOldHead = false; while (queue.length > 0) { const sha = queue.shift(); if (!sha) break; const commit = await store.commit(sha); if (!commit) return null; fresh.push(toIndexed(commit)); if (fresh.length > MAX_INCREMENTAL) return null; for (const parent of commit.parents) { if (seen.has(parent)) continue; seen.add(parent); if (indexed(parent)) { if (parent === oldHead) reachedOldHead = true; } else { queue.push(parent); } } } return reachedOldHead ? fresh : null; }