import { agentCommitter, type Actor } from "./actor"; import { emitEvent } from "./events"; import { countBetween, defaultBranch, fetchBranch, isAncestor, mergeTrees, revParse } from "./git"; import { updateBranch, writeCommit } from "./git-write"; import { assertRefUpdates } from "./protection"; import { afterRefsUpdated } from "./push"; import { findRepoById, forkRepo, markPushed, repoAccess, RepoError, type RepoRecord } from "./repos"; import { getUserById, type User } from "./users"; /* * Forks: copies of a repository that keep a link to it. Changes go back to it through pull requests * from the fork's branches (lib/pulls.ts), and the fork catches up with it by syncing its default * branch. Used by the web UI and MCP, so nothing from next/* here. */ /** Forks `source` for `owner` and tells the source's feed (and webhooks) about it. */ export async function createFork(owner: User, source: RepoRecord, input: { name: string; description: string; defaultBranchOnly: boolean }, actor: Actor) { const fork = await forkRepo(owner, source, input); await emitEvent(source, { kind: "fork", actor, subject: `${fork.owner}/${fork.name}`, payload: { forkee: { full_name: `${fork.owner}/${fork.name}`, owner: fork.owner, name: fork.name, visibility: fork.visibility } }, }); return fork; } export type ForkStatus = { /** The repository it was forked from. */ upstream: RepoRecord; /** The fork's default branch, and the upstream's. */ branch: string; upstreamBranch: string; /** Their commits: both are in the fork. */ sha: string; upstreamSha: string; /** Commits the fork's branch has that the upstream's doesn't, and the other way around. */ ahead: number; behind: number; }; /** * How a fork's default branch compares with the default branch of the repository it was forked * from, or null when there's nothing to compare (an empty side, or an upstream `viewerId` can't read). * Fetches the upstream's new commits into the fork to count them. */ export async function forkStatus(fork: RepoRecord, viewerId: string | null): Promise { const upstream = fork.forkOf && (await findRepoById(fork.forkOf.id)); if (!upstream || !(await repoAccess(upstream, viewerId))) return null; const [branch, upstreamBranch] = await Promise.all([defaultBranch(fork.dir), defaultBranch(upstream.dir)]); if (!branch || !upstreamBranch) return null; const sha = await revParse(fork.dir, `refs/heads/${branch}`); const upstreamSha = await fetchBranch(fork.dir, upstream.dir, upstreamBranch).catch(() => null); if (!sha || !upstreamSha) return null; const [ahead, behind] = await Promise.all([countBetween(fork.dir, upstreamSha, sha), countBetween(fork.dir, sha, upstreamSha)]); return { upstream, branch, upstreamBranch, sha, upstreamSha, ahead, behind }; } /** * Brings the fork's default branch up to date with the upstream's: a fast-forward, or a merge * commit when the fork has commits of its own. Conflicts have to be resolved by hand. */ export async function syncFork(fork: RepoRecord, actor: Actor) { const status = await forkStatus(fork, actor.userId); if (!status) throw new RepoError("There's nothing to sync this repository with."); if (status.behind === 0) return { updated: false, sha: null }; const ref = `refs/heads/${status.branch}`; const { sha: head, upstreamSha } = status; const fastForward = await isAncestor(fork.dir, head, upstreamSha); let commit = upstreamSha; if (!fastForward) { const merged = await mergeTrees(fork.dir, head, upstreamSha); if (!merged.clean) { throw new RepoError( `${status.branch} and ${status.upstream.owner}/${status.upstream.name}:${status.upstreamBranch} change the same lines (${merged.conflicts.slice(0, 5).join(", ")}). Merge them on your machine.`, ); } const user = (await getUserById(actor.userId))!; const person = { name: user.name || user.username, email: user.email }; commit = await writeCommit(fork.dir, { tree: merged.tree, parents: [head, upstreamSha], message: `Merge branch '${status.upstreamBranch}' of ${status.upstream.owner}/${status.upstream.name}\n`, author: person, committer: actor.agent ? agentCommitter(actor.agent) : person, }); } await assertRefUpdates(fork, actor, [{ ref, old: head, new: commit, fastForward: true }]); await updateBranch(fork.dir, status.branch, commit, head, "sync fork"); await markPushed(fork.id); await afterRefsUpdated(fork, new Map([[ref, head]]), new Map([[ref, commit]]), actor); return { updated: true, sha: commit }; }