105 lines · 4.2 KB
import { triggerPush } from "../events/push-trigger";
import { receivePackBody } from "../git/ref-sync-protocol";
import type { RelaySource } from "../git/relay";
import { pushToArtifacts } from "../git/writer";
import { screenMirrorPush } from "../integrations/mirror-screen";
import { CONFLICT_PREFIX, ZERO_SHA } from "./plan";
import { pushToRemote } from "./remote";
import type { Plan } from "./sync";
import type { SyncReport } from "~/lib/mirror";
import type { RepoRow } from "~/lib/types";

const MAX_GATE_PASSES = 3;
const MAX_TRIGGERED_REFS = 50;

function emptyPack(): ReadableStream<Uint8Array> {
  return new ReadableStream({ start: (controller) => controller.close() });
}

/**
 * Applies forge-side updates through the push gates (branch protection as the configuring admin with write
 * permission, push protection), dropping refused refs and re-screening the rest. A refused merge is not
 * pushed to the remote either, so the two sides never end up with different merge results.
 * Applied refs fire normal push events, so CI, webhooks and integrations run. Returns ref → new sha.
 */
export async function applyLocal(input: {
  repo: RepoRow;
  plan: Plan;
  pusherId: string;
  report: SyncReport;
  /** Called before push events fire, so the mirror can recognize its own writes when they come back. */
  onWrite?: (written: Map<string, string>) => Promise<void>;
}): Promise<Map<string, string>> {
  const { repo, plan, report } = input;
  let commands = plan.local;
  let clean = false;
  for (let pass = 0; pass < MAX_GATE_PASSES && commands.length > 0 && !clean; pass++) {
    const screened = await screenMirrorPush(
      repo,
      commands,
      receivePackBody(commands, emptyPack()),