Skip to content
362 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1---
2title: CODEOWNERS
3description: 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.
4---
5
6A **CODEOWNERS** file says who owns which files in a repository. When a
7pull request changes files, g1t asks their owners to review it, shows on
8the pull request whose approval is still needed, and, if the repository
9requires it, holds the merge until they have approved.
10
11```text
12# Everything, unless a later line says otherwise
13* @acme/engineering
14
15# The API belongs to the backend team
16/api/ @acme/backend
17
18# Docs need a writer
19*.md @acme/writers ana@example.com
20
21# Generated code has no owner
22/api/generated/
23```
24
25## Where the file goes
26
27Put the file in one of these places on the repository's default branch.
28g1t reads the first one that exists, in this order, and ignores the rest:
29
30| Order | Path |
31| --- | --- |
32| 1 | `.g1t/CODEOWNERS` |
33| 2 | `.github/CODEOWNERS` |
34| 3 | `CODEOWNERS` |
35| 4 | `docs/CODEOWNERS` |
36| 5 | `.gitlab/CODEOWNERS` |
37
38So a repository you bring to g1t with its file already at
39`.github/CODEOWNERS` or `.gitlab/CODEOWNERS` works as it is. The name is
40case-sensitive: `codeowners` is not read, and neither is a file anywhere
41else, such as `src/CODEOWNERS`.
42
43g1t always reads the file from the branch a pull request merges into, not
44from the pull request. A change to the file takes effect once it is merged.
45
46## Rules
47
48Each line is a **pattern**, then the **owners** of the paths it matches,
49separated by spaces:
50
51```text
52/api/ @ana @acme/backend
53```
54
55For each file, **the last rule that matches it wins**. Put broad rules
56first and narrower ones after them:
57
58```text
59* @acme/engineering
60*.rs @acme/rust
61/src/ @cy
62/src/*.rs @dee
63```
64
65| File | Owners | Because |
66| --- | --- | --- |
67| `README.md` | `@acme/engineering` | Only `*` matches. |
68| `lib/x.rs` | `@acme/rust` | `*.rs` is the last match. |
69| `src/x.txt` | `@cy` | `/src/` is the last match. |
70| `src/x.rs` | `@dee` | `/src/*.rs` is the last match. |
71| `src/a/x.rs` | `@cy` | `/src/*.rs` matches files directly in `src/` only, so `/src/` is the last match. |
72
73The order matters: written the other way round, with `*` last, `@acme/engineering`
74would own everything.
75
76### No owners
77
78A rule with no owners says the paths it matches have none. Their changes
79need no code owner's review:
80
81```text
82* @acme/engineering
83/vendor/
84package-lock.json
85```
86
87### Comments and escapes
88
89| Write | For |
90| --- | --- |
91| `# ...` at the start of a line | A comment line. |
92| ` # ...` after a space | A comment for the rest of the line. |
93| `\#` at the start of a pattern | A literal `#`: `\#notes` matches a file named `#notes`. |
94| `\ ` | A space inside a pattern: `My\ Notes.md`. |
95| `\*`, `\?` | A literal `*` or `?`. |
96
97Blank lines, a byte order mark and Windows line ends (`\r\n`) are all
98fine.
99
100## Patterns
101
102Paths are from the root of the repository, and case-sensitive.
103
104| Pattern | Matches | Does not match |
105| --- | --- | --- |
106| `*` | Every file | |
107| `*.js` | `a.js`, `web/src/a.js` | `a.jsx` |
108| `/*.md` | `README.md` | `docs/README.md` |
109| `?.txt` | `a.txt` | `ab.txt` |
110| `docs` | `docs/a.md`, `x/docs/y/z.md`, a file named `docs` | `mydocs/a.md` |
111| `/docs` | `docs/a.md` | `x/docs/a.md` |
112| `docs/` | `docs/a.md`, `x/docs/a.md` | a file named `docs` |
113| `docs/*` | `docs/a.md` | `docs/sub/a.md`, `x/docs/a.md` |
114| `/build/logs/` | `build/logs/a.txt` | `x/build/logs/a.txt` |
115| `**/foo` | `foo`, `x/foo`, `x/y/foo/bar` | `x/foobar` |
116| `foo/**` | `foo/a`, `foo/a/b` | `x/foo/a` |
117| `a/**/b` | `a/b`, `a/x/b`, `a/x/y/b` | `x/a/b` |
118| `src/**/*.rs` | `src/main.rs`, `src/a/b/main.rs` | `lib/main.rs` |
119| `crates/*/migrations/` | `crates/a/migrations/1.sql` | `crates/a/b/migrations/1.sql` |
120
121In full:
122
123- `*` matches anything but `/`, and `?` one character but `/`.
124- `**/` at the start matches in every directory, `/**` at the end matches
125 everything inside, and `/**/` matches zero or more directories. `**`
126 anywhere else is the same as `*`.
127- A `/` at the start, or anywhere but at the end, anchors the pattern to
128 the root. Without one, it matches at any depth.
129- A `/` at the end means a directory: everything inside one of that name,
130 never a file of that name.
131- A pattern that matches a directory matches every file under it, unless
132 its last part has a wildcard, such as `docs/*` or `*.md`: then it matches
133 files only, so `docs/*` owns `docs/a.md` but not `docs/sub/a.md`.
134
135Negation (`!`) and character ranges (`[a-z]`) are not part of the format.
136A line that uses them is skipped and shown as an [error](#errors). To give
137part of a directory no owner, add a later rule with no owners instead.
138
139## Owners
140
141| Write | Means | Must |
142| --- | --- | --- |
143| `@ana` | The person with that username. | Have the Write [role](/guides/access-and-roles/) or higher on the repository. |
144| `@acme/backend` | Everyone in the [team](/guides/teams/), and in its child teams. | Be a team of the repository's workspace, with the Write role or higher on the repository. |
145| `ana@example.com` | The g1t account that has confirmed that address. | Have the Write role or higher on the repository. |
146| `@g1t` | g1t's agent. | |
147
148A file written for another host may name teams under an organization that
149is not a g1t workspace, such as `@acme-corp/backend`. g1t reads those as
150teams of the repository's own workspace, so creating a team with the slug
151`backend` in it makes the rule work without editing the file. A team of a
152different g1t workspace never owns anything here.
153
154An owner that does not resolve, or cannot write to the repository, owns
155nothing, and is shown as an [error](#errors). A rule left with no owner
156that resolves asks for no review.
157
158## Sections
159
160A line such as `[Docs]` starts a **section**. The rules after it belong to
161it, until the next section header. Rules before the first header are in the
162default section.
163
164Each section applies its own last match. One file can need reviews from
165more than one section:
166
167```text
168*.rb @ruby
169
170[Security]
171config/secrets/ @acme/security
172
173[Database][2] @acme/data
174db/
175db/seeds.rb @seed-keeper @cy
176
177^[Style] @acme/design
178*.css
179```
180
181| File | Needs |
182| --- | --- |
183| `app/user.rb` | 1 approval from `@ruby` |
184| `db/migrate/001.rb` | 1 from `@ruby`, and 2 from `@acme/data` (the section's default owners) |
185| `db/seeds.rb` | 1 from `@ruby`, and 2 from `@seed-keeper` and `@cy` |
186| `config/secrets/prod.yml` | 1 from `@acme/security` |
187| `web/site.css` | Nothing: `@acme/design` is asked, but Style is optional |
188
189| Header | What it does |
190| --- | --- |
191| `[Name]` | Starts a section that needs 1 approval from the owners of each of its rules that match. |
192| `[Name][2]` | Needs that many approvals, from 1 to 10. |
193| `^[Name]` | Optional: its owners are asked to review, but their approval is not required. |
194| `[Name] @owner ...` | Default owners, for the section's rules that name none. |
195
196Section names are compared without regard to case. Two headers with the
197same name are one section: the first spelling is kept, and a later
198header's approval count and default owners replace the earlier ones when
199it gives them. A later `^` makes the section optional.
200
201In a section with default owners, a rule with no owners gets the
202defaults. To say some paths have no owners there, put them in a section
203without defaults.
204
205A header that cannot be read is an error, and the rules after it stay in
206the section before it.
207
208## Review requests
209
210When a pull request is opened, marked ready, or pushed to, g1t works out
211who owns the files it changes and asks them to review it:
212
213- people as reviewers, teams as [team reviewers](/guides/teams/#review-requests),
214 where the team's review assignment decides who is picked;
215- each owner once: someone who was asked and removed is not asked again on
216 the next push;
217- never the pull request's author, or whoever asked g1t for it, and never
218 g1t's agent;
219- owners of optional sections too;
220- a draft once it is marked ready.
221
222The inbox tells them **acme/api#42 changes files you own**, or **acme/api#42
223changes files @acme/backend owns**, with the reason `review_requested`.
224Webhooks get `pull.review_requested` with `data.code_owners` set to `true`.
225
226## On the pull request
227
228A pull request whose target has a CODEOWNERS file shows **Code owners**:
229which rule owns which changed files, each with its section, its owners,
230how many approvals it needs, who has approved, and who has asked for
231changes. A rule that is satisfied, or optional, is marked so.
232
233When the repository requires code owners' approval, the merge box lists
234what is missing:
235
236```text
237@bo asked for changes on /web/ (code owner). Code owners have not approved:
238@acme/backend for /api/, @acme/data for db/ (1 of 2 approvals).
239```
240
241## Require review from code owners
242
243Someone with the Maintain role or higher turns it on under the
244repository's **Settings → Branches and merging**, in **Branch protection**:
245**Require review from code owners**. It is off by default.
246
247With it on, a pull request merges only when every rule that owns a changed
248file has the approvals its section asks for, from its owners, and no code
249owner has asked for changes. It is worked out again at the moment of the
250merge, from the file on the target branch as it is then.
251
252What counts:
253
254| | Counts |
255| --- | --- |
256| An approval from someone the rule names, or from anyone in a team it names or that team's child teams | Yes |
257| An approval from the pull request's author, or from whoever asked g1t for it | Never |
258| An approval from g1t's agent | Only for a rule that names `@g1t`. **g1t's approval counts** does not change this. |
259| A code owner who asked for changes | Holds the merge until that person approves. Only each reviewer's latest verdict counts. |
260| An owner that did not resolve | Owns nothing, so nothing is waited for. |
261
262It holds wherever a pull request merges: the merge button,
263[`merge_pull_request`](/reference/api/pull-requests/merge-pull-request/),
264a g1t agent's [automatic merge](/guides/working-with-g1t/#merging-automatically),
265and the [merge queue](/guides/merge-queue/). It holds pull requests g1t
266opens by itself too, such as [security updates](/guides/security/), which
267then need a person who owns the files. **Allow bypassing required checks**
268does not bypass it.
269
270It adds to **Required approvals**, which is checked first: an approval from
271a code owner also counts towards that number.
272
273```sh
274curl -X PATCH https://api.g1t.sh/repos/acme/api/settings \
275 -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \
276 -d '{"require_code_owner_review": true}'
277```
278
279## Errors
280
281g1t checks the file like a linter, and shows what is wrong with each line:
282
283- on the repository's **Settings → Branches and merging**, in the
284 **CODEOWNERS** panel, for the default branch;
285- on the file's own page, beside each line, when you open it in the code
286 view;
287- through the API, for any branch.
288
289| Kind | Example message |
290| --- | --- |
291| `too_large` | The CODEOWNERS file is larger than 3 MB, so none of it applies; make it smaller. |
292| `negation` | !docs/ starts with !, and negation is not supported; this line is skipped. Give the path a later rule with no owners instead. |
293| `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 ?. |
294| `bad_pattern` | / names no path; this line is skipped. Write a file or directory pattern, such as * or /docs/. |
295| `bad_owner` | nobody is not an owner; write @username, @workspace/team or an email address. |
296| `bad_section` | The approval count [0] must be a whole number from 1 to 10. |
297| `unknown_user` | @ana is not a g1t account. |
298| `unknown_team` | @acme/backend is not a team of acme. |
299| `unknown_email` | No g1t account has confirmed ana@example.com. |
300| `no_write_access` | @bo cannot write to this repository; code owners need the Write role or higher. |
301| `team_no_access` | @acme/docs has no access to this repository; give the team the Write role or higher. |
302
303A section header can also be refused with: "The section header has no
304closing ]; write it as [Name].", "The section header has no name; write it
305as [Name].", "The section name cannot contain [; write it as [Name].", "The
306approval count has no closing ]; write it as [Name][2]." or "Put a space
307between the section header and its owners."
308
309A line with an error in its pattern is skipped. An owner with an error is
310left out, and the rest of its line still applies.
311
312### The codeowners check
313
314A pull request that changes a CODEOWNERS file, in any of the
315[places g1t reads](#where-the-file-goes), gets a status on its head,
316`g1t / codeowners`: a failure such as `.github/CODEOWNERS has 2 errors`,
317or a success, `.github/CODEOWNERS has no errors`. **Details** opens the
318file as the pull request has it, with its errors by line. Make it a
319[required status check](/guides/pull-requests/#required-status-checks) to
320stop a broken file from merging.
321
322## Limits
323
324| | |
325| --- | --- |
326| File size | 3 MB. A larger file is ignored as a whole, with one error. |
327| Approvals per section | 1 to 10. |
328
329## Through the API
330
331| Route | MCP | What it does |
332| --- | --- | --- |
333| `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`. |
334| `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. |
335| `PATCH /repos/{owner}/{name}/settings` | `repository` `update_settings` | `require_code_owner_review`: `true` or `false`. Needs Maintain. |
336
337```sh
338curl "https://api.g1t.sh/repos/acme/api/codeowners/errors?ref=main" \
339 -H "Authorization: Bearer $G1T_TOKEN"
340```
341
342```json
343{
344 "path": ".github/CODEOWNERS",
345 "ref": "main",
346 "size": 412,
347 "rules": 9,
348 "sections": ["Security", "Database"],
349 "errors": [
350 {
351 "line": 7,
352 "kind": "unknown_team",
353 "token": "@acme/backend",
354 "message": "@acme/backend is not a team of acme."
355 }
356 ]
357}
358```
359
360`path` is null when the repository has no CODEOWNERS file at that ref.
361Every route is in the [API reference](/reference/api/), and every action in
362[MCP tools](/reference/mcp/).