Skip to content

Commit 23c02f7

Browse files
srickettsclaude
andcommitted
feat(codeowners): add file-level override support
Allow specifying owners for individual files in the overrides JSON, not just directories. File paths are detected by having an extension. Example overrides: { "flashinfer/fused_moe": ["alice"], "flashinfer/norm.py": ["bob"] } File overrides are written directly to CODEOWNERS without merging, since there's no computed ownership for individual files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 54a94b1 commit 23c02f7

1 file changed

Lines changed: 32 additions & 3 deletions

File tree

scripts/codeowner_analyzer.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,15 @@ def analyze_all_modules(self, verbose: bool = True) -> Dict[str, Any]:
515515

516516
return results
517517

518+
def _is_file_path(self, path: str) -> bool:
519+
"""Check if a path looks like a file (has an extension) vs a directory."""
520+
basename = os.path.basename(path)
521+
return "." in basename and not basename.startswith(".")
522+
523+
def _normalize_usernames(self, users: List[str]) -> List[str]:
524+
"""Normalize usernames to have @ prefix."""
525+
return [f"@{u}" if not u.startswith("@") else u for u in users]
526+
518527
def _merge_owners_with_overrides(
519528
self, module: str, computed_usernames: List[str]
520529
) -> List[str]:
@@ -523,6 +532,9 @@ def _merge_owners_with_overrides(
523532
Override users are prepended to the list (indicating primary ownership),
524533
and duplicates are removed. The final list is limited to top_n_owners.
525534
535+
Note: File-level overrides are handled separately in generate_codeowners_file(),
536+
since there's no computed ownership for individual files.
537+
526538
Args:
527539
module: The module path (e.g., "flashinfer/fused_moe")
528540
computed_usernames: List of GitHub usernames from git history analysis
@@ -534,15 +546,17 @@ def _merge_owners_with_overrides(
534546
if not self.owner_overrides:
535547
return computed_usernames[: self.top_n_owners]
536548

549+
# Skip file-level overrides (handled separately)
550+
if self._is_file_path(module):
551+
return computed_usernames[: self.top_n_owners]
552+
537553
# Get override users for this module (without @ prefix in the config)
538554
override_users = self.owner_overrides.get(module, [])
539555
if not override_users:
540556
return computed_usernames[: self.top_n_owners]
541557

542558
# Normalize override users to have @ prefix
543-
override_usernames = [
544-
f"@{u}" if not u.startswith("@") else u for u in override_users
545-
]
559+
override_usernames = self._normalize_usernames(override_users)
546560

547561
# Build merged list: overrides first, then computed (excluding duplicates)
548562
merged = list(override_usernames)
@@ -565,6 +579,21 @@ def generate_codeowners_file(
565579
f.write("# Manual overrides applied from overrides file\n")
566580
f.write("\n")
567581

582+
# Write file-level overrides first (these are not merged with computed)
583+
if self.owner_overrides:
584+
file_overrides = [
585+
(path, users)
586+
for path, users in self.owner_overrides.items()
587+
if self._is_file_path(path)
588+
]
589+
if file_overrides:
590+
f.write("# File-level overrides\n")
591+
for path, users in sorted(file_overrides):
592+
usernames = self._normalize_usernames(users)
593+
f.write(f"{path} {' '.join(usernames)}\n")
594+
f.write("\n")
595+
596+
# Write directory entries (computed + merged overrides)
568597
for module, data in results.items():
569598
# Extract GitHub usernames from computed owners
570599
computed_usernames = []

0 commit comments

Comments
 (0)