CODEOWNERS semantics as implemented
Resolution
- GitHub: all rules form one list; for a file, the last matching rule wins, and only its owners count. A rule with no owners explicitly unsets ownership.
- GitLab (
--gitlab): rules belong to sections ([Name],[Name][N],^[Name]). Rules before the first header form an unnamed section. The last matching rule per section wins, so a file can have several effective rules, each an independent approval group. Section names are case-insensitive and same-name sections merge (a later header may omit the count and default owners).^marks the section optional: it assigns owners but never blocks a merge request. A rule without owners inherits the section's default owners.
Pattern to regex
Patterns are matched against repo-relative paths with / separators.
| Pattern | Meaning |
|---|---|
*.js |
no slash: matches at any depth |
/*.js |
leading / anchors to the root |
docs/* |
a slash in the middle also anchors; * never crosses /, so docs/a.md matches but docs/sub/a.md does not |
docs/ |
trailing /: a directory, matches everything beneath it, at any depth unless anchored |
docs |
a literal last segment matches a file or a directory (and its contents) at any depth |
docs/** |
everything inside docs |
**/logs |
logs at any depth |
a/**/b |
zero or more directories between a and b |
? |
one character except / |
\#, \ |
literal # and space |
Worked example. docs/* compiles to ^docs/[^/]*$; docs/ compiles to ^(?:.*/)?docs/.*$; /apps/**/test compiles to ^apps/(?:.*/)?test(?:/.*)?$.
Why the last segment matters: a literal last segment (docs) also matches directory contents because a directory is that name. A wildcard last segment (docs/*) does not extend into subdirectories, which is how GitHub documents it. A trailing slash always means "contents of this directory".
Unsupported but parsed (warning RR009): [a-z] ranges (matched literally) and ** embedded in a segment (acts as *). Unsupported and rejected (RR001): leading !.
Owners
@user (alphanumerics and hyphens), @org/team, or an email address. Anything else is RR001; the whole rule is dropped, as GitHub does for invalid lines.
Lint decisions
- Zero-match (RR002): no tracked file matches.
- Shadowed (RR003): the rule matches files, but for every one of them a later rule (same section on GitLab) wins. Partially shadowed rules are fine. Shadowing caused by a misplaced catch-all (RR007) or by an identical later pattern (RR006) is reported once, under that root cause, to avoid noise.
- Catch-all (RR007): a pattern matching both a root file and a deeply nested file (
*,**,/**,**/*) that appears after non-catch-all rules in its section. - Stale owner (RR008): for each rule, take the files it effectively owns; if no member of the owner (a user, or a roster team's members) committed to any of them within
--inactive-days, warn. Owners already flagged unknown or inactive are skipped.