154 lines · 6.2 KB
import { env, waitUntil } from "cloudflare:workers";
import { all, chunks, placeholders } from "./db";
import { withStore } from "./git/store";
import type { GitStore } from "./git/store";
import { diffTreesBounded } from "./git/diff";
import { compareCommits } from "./git/history";
import { globToRegExp } from "~/lib/glob";
import type { RepoRow } from "~/lib/types";
import type { PullRow } from "./pulls";

const CODEOWNERS_PATHS = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"];
const MAX_CODEOWNERS_BYTES = 64 * 1024;
const MAX_RULES = 500;
const MAX_OWNED_PATHS = 300;
const MAX_REQUESTED_OWNERS = 15;
const OWNED_CACHE_TTL_SECONDS = 60 * 60 * 24 * 7;
const USER_OWNER = /^@([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))$/;
const decoder = new TextDecoder();

type Rule = { pattern: RegExp; owners: string[] };

function patternToRegExp(raw: string): RegExp {
  const directoryOnly = raw.endsWith("/");
  let pattern = directoryOnly ? raw.slice(0, -1) : raw;
  const anchored = pattern.startsWith("/") || pattern.includes("/");
  pattern = pattern.replace(/^\//, "");
  const body = globToRegExp(pattern).source.slice(1, -1);
  const suffix = directoryOnly ? "/.*" : pattern.endsWith("/*") ? "" : "(?:/.*)?";
  return new RegExp(`${anchored ? "^" : "^(?:.*/)?"}${body}${suffix}$`);
}

/** Parses CODEOWNERS (gitignore-style patterns, last match wins); team and email owners are ignored. */
export function parseCodeOwners(source: string): Rule[] {
  const rules: Rule[] = [];
  for (const line of source.split("\n")) {
    const trimmed = line.replace(/(^|\s)#.*$/, "").trim();
    if (!trimmed) continue;
    const [pattern, ...owners] = trimmed.split(/\s+/);
    rules.push({
      pattern: patternToRegExp(pattern),