import { eq } from "drizzle-orm";
import { db } from "../db";
import { codeOwners } from "../db/schema";
export interface OwnerRule {
pattern: string;
owners: string[];
}
export function parseCodeowners(content: string): OwnerRule[] {
const rules: OwnerRule[] = [];
for (const rawLine of content.split("\n")) {
const line = rawLine.replace(/#.*$/, "").trim();
if (!line) continue;
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
const pattern = parts[0];
const owners = parts
.slice(1)
.map((o) => o.replace(/^@/, "").trim())
.filter(Boolean);
if (owners.length === 0) continue;
rules.push({ pattern, owners });
}
return rules;
}
function patternToRegex(pattern: string): RegExp {
const anchored = pattern.startsWith("/");
let p = anchored ? pattern.slice(1) : pattern;
p = p.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
p = p.replace(/\*\*/g, "__DOUBLEGLOB__");
p = p.replace(/\*/g, "[^/]*");
p = p.replace(/__DOUBLEGLOB__/g, ".*");
const prefix = anchored ? "^" : "^(?:.*/)?";
const suffix = p.endsWith("/") ? ".*$" : "(?:/.*)?$";
return new RegExp(prefix + p + suffix);
}
export function ownersForPath(
path: string,
rules: OwnerRule[]
): string[] {
let matched: string[] = [];
for (const r of rules) {
if (patternToRegex(r.pattern).test(path)) {
matched = r.owners;
}
}
return matched;
}
export async function syncCodeowners(
repositoryId: string,
rules: OwnerRule[]
): Promise<void> {
try {
await db.delete(codeOwners).where(eq(codeOwners.repositoryId, repositoryId));
if (rules.length === 0) return;
await db.insert(codeOwners).values(
rules.map((r) => ({
repositoryId,
pathPattern: r.pattern,
ownerUsernames: r.owners.join(","),
}))
);
} catch (err) {
console.error("[codeowners] sync failed:", err);
}
}
export async function reviewersForChangedFiles(
repositoryId: string,
paths: string[]
): Promise<string[]> {
try {
const rules = await db
.select()
.from(codeOwners)
.where(eq(codeOwners.repositoryId, repositoryId));
const parsed: OwnerRule[] = rules.map((r) => ({
pattern: r.pathPattern,
owners: r.ownerUsernames.split(",").filter(Boolean),
}));
const result = new Set<string>();
for (const p of paths) {
for (const u of ownersForPath(p, parsed)) result.add(u);
}
return [...result];
} catch {
return [];
}
}
|