CODEOWNERS
Say who owns which paths in a repository with a CODEOWNERS file. g1t asks owners to review the pull requests that change their files, and branch protection can require their approval before a merge.
A CODEOWNERS file says who owns which files in a repository. When a pull request changes files, g1t asks their owners to review it, shows on the pull request whose approval is still needed, and, if the repository requires it, holds the merge until they have approved.
# Everything, unless a later line says otherwise* @acme/engineering
# The API belongs to the backend team/api/ @acme/backend
# Docs need a writer*.md @acme/writers ana@example.com
# Generated code has no owner/api/generated/Where the file goes
Section titled “Where the file goes”Put the file in one of these places on the repository’s default branch. g1t reads the first one that exists, in this order, and ignores the rest:
| Order | Path |
|---|---|
| 1 | .g1t/CODEOWNERS |
| 2 | .github/CODEOWNERS |
| 3 | CODEOWNERS |
| 4 | docs/CODEOWNERS |
| 5 | .gitlab/CODEOWNERS |
So a repository you bring to g1t with its file already at
.github/CODEOWNERS or .gitlab/CODEOWNERS works as it is. The name is
case-sensitive: codeowners is not read, and neither is a file anywhere
else, such as src/CODEOWNERS.
g1t always reads the file from the branch a pull request merges into, not from the pull request. A change to the file takes effect once it is merged.
Each line is a pattern, then the owners of the paths it matches, separated by spaces:
/api/ @ana @acme/backendFor each file, the last rule that matches it wins. Put broad rules first and narrower ones after them:
* @acme/engineering*.rs @acme/rust/src/ @cy/src/*.rs @dee| File | Owners | Because |
|---|---|---|
README.md |
@acme/engineering |
Only * matches. |
lib/x.rs |
@acme/rust |
*.rs is the last match. |
src/x.txt |
@cy |
/src/ is the last match. |
src/x.rs |
@dee |
/src/*.rs is the last match. |
src/a/x.rs |
@cy |
/src/*.rs matches files directly in src/ only, so /src/ is the last match. |
The order matters: written the other way round, with * last, @acme/engineering
would own everything.
No owners
Section titled “No owners”A rule with no owners says the paths it matches have none. Their changes need no code owner’s review:
* @acme/engineering/vendor/package-lock.jsonComments and escapes
Section titled “Comments and escapes”| Write | For |
|---|---|
# ... at the start of a line |
A comment line. |
# ... after a space |
A comment for the rest of the line. |
\# at the start of a pattern |
A literal #: \#notes matches a file named #notes. |
\ |
A space inside a pattern: My\ Notes.md. |
\*, \? |
A literal * or ?. |
Blank lines, a byte order mark and Windows line ends (\r\n) are all
fine.
Patterns
Section titled “Patterns”Paths are from the root of the repository, and case-sensitive.
| Pattern | Matches | Does not match |
|---|---|---|
* |
Every file | |
*.js |
a.js, web/src/a.js |
a.jsx |
/*.md |
README.md |
docs/README.md |
?.txt |
a.txt |
ab.txt |
docs |
docs/a.md, x/docs/y/z.md, a file named docs |
mydocs/a.md |
/docs |
docs/a.md |
x/docs/a.md |
docs/ |
docs/a.md, x/docs/a.md |
a file named docs |
docs/* |
docs/a.md |
docs/sub/a.md, x/docs/a.md |
/build/logs/ |
build/logs/a.txt |
x/build/logs/a.txt |
**/foo |
foo, x/foo, x/y/foo/bar |
x/foobar |
foo/** |
foo/a, foo/a/b |
x/foo/a |
a/**/b |
a/b, a/x/b, a/x/y/b |
x/a/b |
src/**/*.rs |
src/main.rs, src/a/b/main.rs |
lib/main.rs |
crates/*/migrations/ |
crates/a/migrations/1.sql |
crates/a/b/migrations/1.sql |
In full:
*matches anything but/, and?one character but/.**/at the start matches in every directory,/**at the end matches everything inside, and/**/matches zero or more directories.**anywhere else is the same as*.- A
/at the start, or anywhere but at the end, anchors the pattern to the root. Without one, it matches at any depth. - A
/at the end means a directory: everything inside one of that name, never a file of that name. - A pattern that matches a directory matches every file under it, unless
its last part has a wildcard, such as
docs/*or*.md: then it matches files only, sodocs/*ownsdocs/a.mdbut notdocs/sub/a.md.
Negation (!) and character ranges ([a-z]) are not part of the format.
A line that uses them is skipped and shown as an error. To give
part of a directory no owner, add a later rule with no owners instead.
Owners
Section titled “Owners”| Write | Means | Must |
|---|---|---|
@ana |
The person with that username. | Have the Write role or higher on the repository. |
@acme/backend |
Everyone in the team, and in its child teams. | Be a team of the repository’s workspace, with the Write role or higher on the repository. |
ana@example.com |
The g1t account that has confirmed that address. | Have the Write role or higher on the repository. |
@g1t |
g1t’s agent. |
A file written for another host may name teams under an organization that
is not a g1t workspace, such as @acme-corp/backend. g1t reads those as
teams of the repository’s own workspace, so creating a team with the slug
backend in it makes the rule work without editing the file. A team of a
different g1t workspace never owns anything here.
An owner that does not resolve, or cannot write to the repository, owns nothing, and is shown as an error. A rule left with no owner that resolves asks for no review.
Sections
Section titled “Sections”A line such as [Docs] starts a section. The rules after it belong to
it, until the next section header. Rules before the first header are in the
default section.
Each section applies its own last match. One file can need reviews from more than one section:
*.rb @ruby
[Security]config/secrets/ @acme/security
[Database][2] @acme/datadb/db/seeds.rb @seed-keeper @cy
^[Style] @acme/design*.css| File | Needs |
|---|---|
app/user.rb |
1 approval from @ruby |
db/migrate/001.rb |
1 from @ruby, and 2 from @acme/data (the section’s default owners) |
db/seeds.rb |
1 from @ruby, and 2 from @seed-keeper and @cy |
config/secrets/prod.yml |
1 from @acme/security |
web/site.css |
Nothing: @acme/design is asked, but Style is optional |
| Header | What it does |
|---|---|
[Name] |
Starts a section that needs 1 approval from the owners of each of its rules that match. |
[Name][2] |
Needs that many approvals, from 1 to 10. |
^[Name] |
Optional: its owners are asked to review, but their approval is not required. |
[Name] @owner ... |
Default owners, for the section’s rules that name none. |
Section names are compared without regard to case. Two headers with the
same name are one section: the first spelling is kept, and a later
header’s approval count and default owners replace the earlier ones when
it gives them. A later ^ makes the section optional.
In a section with default owners, a rule with no owners gets the defaults. To say some paths have no owners there, put them in a section without defaults.
A header that cannot be read is an error, and the rules after it stay in the section before it.
Review requests
Section titled “Review requests”When a pull request is opened, marked ready, or pushed to, g1t works out who owns the files it changes and asks them to review it:
- people as reviewers, teams as team reviewers, where the team’s review assignment decides who is picked;
- each owner once: someone who was asked and removed is not asked again on the next push;
- never the pull request’s author, or whoever asked g1t for it, and never g1t’s agent;
- owners of optional sections too;
- a draft once it is marked ready.
The inbox tells them acme/api#42 changes files you own, or acme/api#42
changes files @acme/backend owns, with the reason review_requested.
Webhooks get pull.review_requested with data.code_owners set to true.
On the pull request
Section titled “On the pull request”A pull request whose target has a CODEOWNERS file shows Code owners: which rule owns which changed files, each with its section, its owners, how many approvals it needs, who has approved, and who has asked for changes. A rule that is satisfied, or optional, is marked so.
When the repository requires code owners’ approval, the merge box lists what is missing:
@bo asked for changes on /web/ (code owner). Code owners have not approved:@acme/backend for /api/, @acme/data for db/ (1 of 2 approvals).Require review from code owners
Section titled “Require review from code owners”Someone with the Admin role turns it on under the
repository’s Settings → Rules, in a ruleset’s Require a pull request before merging rule:
Require review from code owners. It is off by default. From the API it
is the pull_request rule’s require_code_owner_review (see rules).
With it on, a pull request merges only when every rule that owns a changed file has the approvals its section asks for, from its owners, and no code owner has asked for changes. It is worked out again at the moment of the merge, from the file on the target branch as it is then.
What counts:
| Counts | |
|---|---|
| An approval from someone the rule names, or from anyone in a team it names or that team’s child teams | Yes |
| An approval from the pull request’s author, or from whoever asked g1t for it | Never |
| An approval from g1t’s agent | Only for a rule that names @g1t. g1t’s approval counts does not change this. |
| A code owner who asked for changes | Holds the merge until that person approves. Only each reviewer’s latest verdict counts. |
| An owner that did not resolve | Owns nothing, so nothing is waited for. |
It holds wherever a pull request merges: the merge button,
merge_pull_request,
a g1t agent’s automatic merge,
and the merge queue. It holds pull requests g1t
opens by itself too, such as security updates, which
then need a person who owns the files. Allow bypassing required checks
does not bypass it.
It adds to Required approvals, which is checked first: an approval from a code owner also counts towards that number.
curl -X PATCH https://api.g1t.sh/repos/acme/api/settings \ -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \ -d '{"require_code_owner_review": true}'Errors
Section titled “Errors”g1t checks the file like a linter, and shows what is wrong with each line:
- on the repository’s Settings → Branches and merging, in the CODEOWNERS panel, for the default branch;
- on the file’s own page, beside each line, when you open it in the code view;
- through the API, for any branch.
| Kind | Example message |
|---|---|
too_large |
The CODEOWNERS file is larger than 3 MB, so none of it applies; make it smaller. |
negation |
!docs/ starts with !, and negation is not supported; this line is skipped. Give the path a later rule with no owners instead. |
character_range |
docs/[a-z]*.md uses [ or ], and character ranges are not supported; this line is skipped. Write one rule per name, or use * or ?. |
bad_pattern |
/ names no path; this line is skipped. Write a file or directory pattern, such as * or /docs/. |
bad_owner |
nobody is not an owner; write @username, @workspace/team or an email address. |
bad_section |
The approval count [0] must be a whole number from 1 to 10. |
unknown_user |
@ana is not a g1t account. |
unknown_team |
@acme/backend is not a team of acme. |
unknown_email |
No g1t account has confirmed ana@example.com. |
no_write_access |
@bo cannot write to this repository; code owners need the Write role or higher. |
team_no_access |
@acme/docs has no access to this repository; give the team the Write role or higher. |
A section header can also be refused with: “The section header has no closing ]; write it as [Name].”, “The section header has no name; write it as [Name].”, “The section name cannot contain [; write it as [Name].”, “The approval count has no closing ]; write it as [Name][2].” or “Put a space between the section header and its owners.”
A line with an error in its pattern is skipped. An owner with an error is left out, and the rest of its line still applies.
The codeowners check
Section titled “The codeowners check”A pull request that changes a CODEOWNERS file, in any of the
places g1t reads, gets a status on its head,
g1t / codeowners: a failure such as .github/CODEOWNERS has 2 errors,
or a success, .github/CODEOWNERS has no errors. Details opens the
file as the pull request has it, with its errors by line. Make it a
required status check to
stop a broken file from merging.
Limits
Section titled “Limits”| File size | 3 MB. A larger file is ignored as a whole, with one error. |
| Approvals per section | 1 to 10. |
Through the API
Section titled “Through the API”| Route | MCP | What it does |
|---|---|---|
GET /repos/{owner}/{name}/codeowners/errors |
repository codeowners |
The file at ref (the default branch when left out): where it is, its size, its rules and sections, and every error. Needs repo:read. |
GET /repos/{owner}/{name}/pulls/{number} |
pull_request get |
code_owners: the file’s path, required, a review per rule with section, pattern, owners, files, required, approved_by, changes_requested_by and satisfied, what is missing, and how many errors the file has. Absent when the target has no file. |
PATCH /repos/{owner}/{name}/settings |
repository update_settings |
require_code_owner_review: true or false. Needs Admin. |
curl "https://api.g1t.sh/repos/acme/api/codeowners/errors?ref=main" \ -H "Authorization: Bearer $G1T_TOKEN"{ "path": ".github/CODEOWNERS", "ref": "main", "size": 412, "rules": 9, "sections": ["Security", "Database"], "errors": [ { "line": 7, "kind": "unknown_team", "token": "@acme/backend", "message": "@acme/backend is not a team of acme." } ]}path is null when the repository has no CODEOWNERS file at that ref.
Every route is in the API reference, and every action in
MCP tools.