97 lines · 4.2 KB
import { checkProtections } from "../git/proxy";
import { findForcePushes } from "../git/force-push";
import { isHiddenRef } from "../git/hidden-refs";
import type { RefCommand } from "../git/ref-sync-protocol";
import type { ScreenResult } from "../git/relay";
import { scanPushBody } from "../security/push-protection";
import type { RepoRow } from "~/lib/types";

const ZERO_SHA = "0".repeat(40);
const HIDDEN = "refs/pull and refs/gitfor are managed by gitfor";
const FAST_FORWARD_ONLY = "mirror is fast-forward only; an admin can enable force mirroring";
const SECRETS = "push declined due to secrets";

export type MirrorPolicy = { force: boolean; pusherId: string | null };

/** The push gates, injectable so the mirror policy can be tested without a repository store. */
export type MirrorGates = {
  checkProtections: typeof checkProtections;
  findForcePushes: typeof findForcePushes;
  scanPushBody: typeof scanPushBody;
};

const DEFAULT_GATES: MirrorGates = { checkProtections, findForcePushes, scanPushBody };

/** Hidden refs are dropped before anything is fetched: a mirror can never write gitfor's bookkeeping refs. */
export function withoutHiddenRefs(commands: RefCommand[]): { commands: RefCommand[]; rejected: Map<string, string> } {
  const rejected = new Map<string, string>();
  const kept = commands.filter((command) => {
    if (!isHiddenRef(command.name)) return true;
    rejected.set(command.name, HIDDEN);
    return false;
  });
  return { commands: kept, rejected };
}

/** Updates of existing refs (not creations or deletions), the ones that can be non-fast-forward. */
/** Refusals that will repeat on every run until someone changes a rule or the upstream history. */
export function isPolicyRefusal(reason: string): boolean {
  return (
    [HIDDEN, FAST_FORWARD_ONLY, SECRETS, "protected"].some((marker) => reason.includes(marker)) &&