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<ForkStatus | null> {
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 };
}