101 lines · 4.2 KB
import { env } from "cloudflare:workers";
import { mergeBudget, mergeRootTrees } from "../git/merge";
import type { MergeConflict } from "../git/merge";
import { MergeTooComplexError } from "../git/merge3";
import { makeCommit, signatureNow } from "../git/objects";
import type { RawObject } from "../git/objects";
import { compareCommits } from "../git/history";
import type { GitStore } from "../git/store";
import { publicUrl } from "../events/emit";
import { oursResolutions } from "./resolve";
import type { Relation } from "./plan";
import type { ConflictStrategy } from "~/lib/mirror";

const MAX_LISTED_PATHS = 20;

/** How `local` and `remote` relate, from their merge base. */
export async function relate(store: GitStore, local: string, remote: string): Promise<Relation> {
  const comparison = await compareCommits(store, local, store, remote);
  if (comparison.mergeBase === null) return comparison.truncated ? "unknown" : "unrelated";
  if (comparison.mergeBase === local) return "remote-ahead";
  if (comparison.mergeBase === remote) return "local-ahead";
  return "diverged";
}

export type MirrorMerge =
  | { ok: true; sha: string; objects: RawObject[]; resolved: string[] }
  | { ok: false; reason: string; paths: string[] };

function mirrorSignature() {
  const host = new URL(publicUrl("/")).hostname;
  return signatureNow(`${env.FORGE_NAME || "gitfor"} mirror`, `mirror@${host}`);
}

function describe(conflicts: MergeConflict[]): string[] {
  return conflicts.slice(0, MAX_LISTED_PATHS).map((conflict) => `${conflict.path} (${conflict.kind})`);
}

/**
 * Merges diverged `local` and `remote` tips into one commit both sides can fast-forward to. Non-overlapping
 * changes merge cleanly; overlapping hunks either stop the merge (`merge`) or keep the preferred side's lines