Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

codeowners.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

codeowners.tsBlame120 lines · 1 contributor
3ef4c9dClaude1/**
2 * CODEOWNERS parser + sync.
3 *
4 * Parses a CODEOWNERS file (GitHub-compatible syntax):
5 * # comments allowed
6 * * @alice
7 * src/api/** @bob @carol
8 * /docs @alice
9 *
10 * Ownership is resolved by last-matching rule (that's how GitHub does it).
11 */
12
13import { eq } from "drizzle-orm";
14import { db } from "../db";
15import { codeOwners } from "../db/schema";
16
17export interface OwnerRule {
18 pattern: string;
19 owners: string[]; // usernames, stripped of leading @
20}
21
22export function parseCodeowners(content: string): OwnerRule[] {
23 const rules: OwnerRule[] = [];
24 for (const rawLine of content.split("\n")) {
25 const line = rawLine.replace(/#.*$/, "").trim();
26 if (!line) continue;
27 const parts = line.split(/\s+/);
28 if (parts.length < 2) continue;
29 const pattern = parts[0];
30 const owners = parts
31 .slice(1)
32 .map((o) => o.replace(/^@/, "").trim())
33 .filter(Boolean);
34 if (owners.length === 0) continue;
35 rules.push({ pattern, owners });
36 }
37 return rules;
38}
39
40/**
41 * Glob-to-regex for CODEOWNERS patterns. Supports `*` and `**`.
42 * Patterns anchored at the repo root if they start with `/`.
43 */
44function patternToRegex(pattern: string): RegExp {
45 const anchored = pattern.startsWith("/");
46 let p = anchored ? pattern.slice(1) : pattern;
47 // Escape regex metacharacters except * and /
48 p = p.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
49 p = p.replace(/\*\*/g, "__DOUBLEGLOB__");
50 p = p.replace(/\*/g, "[^/]*");
51 p = p.replace(/__DOUBLEGLOB__/g, ".*");
52 const prefix = anchored ? "^" : "^(?:.*/)?";
53 const suffix = p.endsWith("/") ? ".*$" : "(?:/.*)?$";
54 return new RegExp(prefix + p + suffix);
55}
56
57/**
58 * Return owner usernames for a given file path. Last matching rule wins.
59 */
60export function ownersForPath(
61 path: string,
62 rules: OwnerRule[]
63): string[] {
64 let matched: string[] = [];
65 for (const r of rules) {
66 if (patternToRegex(r.pattern).test(path)) {
67 matched = r.owners;
68 }
69 }
70 return matched;
71}
72
73/**
74 * Replace all rules for a repo in the DB.
75 */
76export async function syncCodeowners(
77 repositoryId: string,
78 rules: OwnerRule[]
79): Promise<void> {
80 try {
81 await db.delete(codeOwners).where(eq(codeOwners.repositoryId, repositoryId));
82 if (rules.length === 0) return;
83 await db.insert(codeOwners).values(
84 rules.map((r) => ({
85 repositoryId,
86 pathPattern: r.pattern,
87 ownerUsernames: r.owners.join(","),
88 }))
89 );
90 } catch (err) {
91 console.error("[codeowners] sync failed:", err);
92 }
93}
94
95/**
96 * Given a PR's changed file list, return all unique owner usernames to
97 * auto-request review from.
98 */
99export async function reviewersForChangedFiles(
100 repositoryId: string,
101 paths: string[]
102): Promise<string[]> {
103 try {
104 const rules = await db
105 .select()
106 .from(codeOwners)
107 .where(eq(codeOwners.repositoryId, repositoryId));
108 const parsed: OwnerRule[] = rules.map((r) => ({
109 pattern: r.pathPattern,
110 owners: r.ownerUsernames.split(",").filter(Boolean),
111 }));
112 const result = new Set<string>();
113 for (const p of paths) {
114 for (const u of ownersForPath(p, parsed)) result.add(u);
115 }
116 return [...result];
117 } catch {
118 return [];
119 }
120}