import { loadUserById, toViewer } from "../auth"; import { hasPermission, repoPermission } from "../repos"; import type { RepoRow } from "~/lib/types"; /** Copying must never widen visibility: private objects may only land in a private repository. */ export function canBridge(from: RepoRow, to: RepoRow): boolean { return from.visibility === "public" || to.visibility === "private"; } /** * Base commits copied into the head repo become readable by everyone who can read the head repo, which * is not a subset of base readers (not even with the same owner: collaborators differ). Only public * bases may flow there; private bases use the git container, which never stores objects in the fork. */ export function canBridgeBase(baseRepo: RepoRow): boolean { return baseRepo.visibility === "public"; } /** A pull request head must be the base itself or a live (not detached) direct fork of the base. */ export function isPullHeadRepo( baseRepo: Pick, headRepo: Pick, ): boolean { return headRepo.id === baseRepo.id || (headRepo.fork_of_id === baseRepo.id && headRepo.detached !== 1); } export type PullAccessSubject = { author_id: string }; /** * Whether a fork pull request may still move objects from its head into its base: the head must be a * fork of the base, the copy must not widen visibility, and the author must currently be able to read * both repositories. A reader who was removed from the base therefore stops receiving its history * (and stops feeding theirs) the moment access is revoked, not when someone notices. */ export async function pullHeadAccessible( pull: PullAccessSubject, baseRepo: RepoRow, headRepo: RepoRow, ): Promise { if (baseRepo.id === headRepo.id) return true; if (!isPullHeadRepo(baseRepo, headRepo)) return false; if (!canBridge(headRepo, baseRepo)) return false; const user = await loadUserById(pull.author_id); if (!user) return false; const viewer = toViewer(user); const [onBase, onHead] = await Promise.all([repoPermission(baseRepo, viewer), repoPermission(headRepo, viewer)]); return hasPermission(onBase, "read") && hasPermission(onHead, "read"); }