80 lines · 3.4 KB
import { badRequest, forbidden } from "../errors";
import type { GitStore } from "../git/store";
import { MAX_POINTER_BYTES, parseLfsPointer } from "../lfs/pointer";
import { readBounded } from "./blob-read";
import { attributeGlob } from "~/lib/gitattributes";

const MAX_ATTRIBUTES_BYTES = 64 * 1024;
const MAX_ATTRIBUTE_RULES = 500;
const decoder = new TextDecoder();

type Rule = { regex: RegExp; anchored: boolean; lfs: boolean };

/** Root `.gitattributes` rules that set or unset `filter=lfs`; later rules win, as in git. */
async function lfsRules(store: GitStore, rootTree: string): Promise<Rule[]> {
  const entry = await store.entryAtPath(rootTree, ".gitattributes");
  if (!entry || entry.type === "tree" || entry.type === "gitlink") return [];
  const bytes = (await readBounded(store, entry.sha, MAX_ATTRIBUTES_BYTES))?.bytes;
  if (!bytes) return [];
  const rules: Rule[] = [];
  for (const line of decoder.decode(bytes).split("\n").slice(0, MAX_ATTRIBUTE_RULES)) {
    const [pattern, ...attributes] = line.trim().split(/\s+/);
    if (!pattern || pattern.startsWith("#")) continue;
    const lfs = attributes.includes("filter=lfs");
    if (!lfs && !attributes.some((attribute) => attribute === "-filter" || attribute === "!filter")) continue;
    const anchored = pattern.includes("/");
    rules.push({ regex: attributeGlob(pattern.replace(/^\//, "")), anchored, lfs });
  }
  return rules;
}

function isLfsTracked(rules: Rule[], path: string): boolean {
  let tracked = false;
  const basename = path.slice(path.lastIndexOf("/") + 1);
  for (const rule of rules) if (rule.regex.test(rule.anchored ? path : basename)) tracked = rule.lfs;
  return tracked;
}

function assertDistinct(paths: string[]): void {
  const seen = new Set<string>();
  for (const path of paths) {