138 lines · 6.5 KB
import { compareBranches } from "./git/compare";
import { diffTreesCached } from "./git/diff";
import { compareCommits } from "./git/history";
import { isReachableCommit } from "./git/reachable";
import { listRefs } from "./git/refs";
import { safeDecode } from "~/lib/safe-decode";
import { withStore } from "./git/store";
import type { GitStore } from "./git/store";
import { HttpError, notFoundError } from "./errors";
import { limitHeavy } from "./limits";
import { loadRepoById } from "./repos";
import { loadPull } from "./pulls";
import { formatPatchSeries, patchResponse } from "./patch-format";
import type { PatchKind } from "./patch-format";
import type { CommitSummary, RepoRow, Viewer } from "~/lib/types";

const COMMIT_PATH = /^\/([^/]+)\/([^/]+)\/commit\/([0-9a-f]{4,40})\.(diff|patch)$/;
const PULL_PATH = /^\/([^/]+)\/([^/]+)\/pull\/(\d+)\.(diff|patch)$/;
const COMPARE_PATH = /^\/([^/]+)\/([^/]+)\/compare\/(.+)\.(diff|patch)$/;
/** Each commit in a series is its own tree diff, so long series are refused rather than half-rendered. */
const MAX_SERIES_COMMITS = 100;
const TOO_LARGE = 413;
const FULL_SHA_LENGTH = 40;

export type RepoAccess = (request: Request, owner: string, name: string) => Promise<RepoRow | Response>;

function tooLarge(): never {
  throw new HttpError(TOO_LARGE, "This diff is too large to render; fetch the commits with git instead");
}

async function commitPatch(store: GitStore, commit: CommitSummary): Promise<string> {
  const parent = commit.parents[0] ? await store.commit(commit.parents[0]) : null;
  const diff = await diffTreesCached(store, parent?.treeSha ?? null, commit.treeSha);
  if (diff.truncated) tooLarge();
  return diff.patch;
}

/** `git format-patch` output for a range: oldest first, merge commits skipped like format-patch does. */
async function series(store: GitStore, commits: CommitSummary[], truncated: boolean): Promise<string> {
  const ordered = commits.filter((commit) => commit.parents.length <= 1).sort((a, b) => a.committedAt - b.committedAt);