Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

359 linesCodeBlameRaw
1---
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
147A file written for another host may name teams under an organization that
148is not a g1t workspace, such as `@acme-corp/backend`. g1t reads those as
149teams of the repository's own workspace, so creating a team with the slug
150`backend` in it makes the rule work without editing the file. A team of a
151different g1t workspace never owns anything here.
152
153An owner that does not resolve, or cannot write to the repository, owns
154nothing, and is shown as an [error](#errors). A rule left with no owner
155that resolves asks for no review.
156
157## Sections
158
159A line such as `[Docs]` starts a **section**. The rules after it belong to
160it, until the next section header. Rules before the first header are in the
161default section.
162
163Each section applies its own last match. One file can need reviews from
164more than one section:
165
166```text
167*.rb @ruby
168
169[Security]
170config/secrets/ @acme/security
171
172[Database][2] @acme/data
173db/
174db/seeds.rb @seed-keeper @cy
175
176^[Style] @acme/design
177*.css
178```
179
180| File | Needs |
181| --- | --- |
182| `app/user.rb` | 1 approval from `@ruby` |
183| `db/migrate/001.rb` | 1 from `@ruby`, and 2 from `@acme/data` (the section's default owners) |
184| `db/seeds.rb` | 1 from `@ruby`, and 2 from `@seed-keeper` and `@cy` |
185| `config/secrets/prod.yml` | 1 from `@acme/security` |
186| `web/site.css` | Nothing: `@acme/design` is asked, but Style is optional |
187
188| Header | What it does |
189| --- | --- |
190| `[Name]` | Starts a section that needs 1 approval from the owners of each of its rules that match. |
191| `[Name][2]` | Needs that many approvals, from 1 to 10. |
192| `^[Name]` | Optional: its owners are asked to review, but their approval is not required. |
193| `[Name] @owner ...` | Default owners, for the section's rules that name none. |
194
195Section names are compared without regard to case. Two headers with the
196same name are one section: the first spelling is kept, and a later
197header's approval count and default owners replace the earlier ones when
198it gives them. A later `^` makes the section optional.
199
200In a section with default owners, a rule with no owners gets the
201defaults. To say some paths have no owners there, put them in a section
202without defaults.
203
204A header that cannot be read is an error, and the rules after it stay in
205the section before it.
206
207## Review requests
208
209When a pull request is opened, marked ready, or pushed to, g1t works out
210who owns the files it changes and asks them to review it:
211
212- people as reviewers, teams as [team reviewers](/guides/teams/#review-requests),
213 where the team's review assignment decides who is picked;
214- each owner once: someone who was asked and removed is not asked again on
215 the next push;
216- never the pull request's author;
217- owners of optional sections too;
218- a draft once it is marked ready.
219
220Notifications tell them **acme/api#42 changes files you own**, or **acme/api#42
221changes files @acme/backend owns**, with the reason `review_requested`.
222Webhooks get `pull.review_requested` with `data.code_owners` set to `true`.
223
224## On the pull request
225
226A pull request whose target has a CODEOWNERS file shows **Code owners**:
227which rule owns which changed files, each with its section, its owners,
228how many approvals it needs, who has approved, and who has asked for
229changes. A rule that is satisfied, or optional, is marked so.
230
231When the repository requires code owners' approval, the merge box lists
232what is missing:
233
234```text
235@bo asked for changes on /web/ (code owner). Code owners have not approved:
236@acme/backend for /api/, @acme/data for db/ (1 of 2 approvals).
237```
238
239## Require review from code owners
240
241Someone with the Admin role turns it on under the
242repository's **Settings → Rules**, in a ruleset's **Require a pull request before merging** rule:
243**Require review from code owners**. It is off by default. From the API it
244is the `pull_request` rule's `require_code_owner_review` (see [rules](/guides/rules/)).
245
246With it on, a pull request merges only when every rule that owns a changed
247file has the approvals its section asks for, from its owners, and no code
248owner has asked for changes. It is worked out again at the moment of the
249merge, from the file on the target branch as it is then.
250
251What counts:
252
253| | Counts |
254| --- | --- |
255| An approval from someone the rule names, or from anyone in a team it names or that team's child teams | Yes |
256| An approval from the pull request's author | Never |
257| A code owner who asked for changes | Holds the merge until that person approves. Only each reviewer's latest verdict counts. |
258| An owner that did not resolve | Owns nothing, so nothing is waited for. |
259
260It holds wherever a pull request merges: the merge button,
261[`merge_pull_request`](/reference/api/pull-requests/merge-pull-request/),
262an automatic merge and the [merge queue](/guides/merge-queue/). It holds pull requests g1t
263opens by itself too, such as [security updates](/guides/security/), which
264then need a person who owns the files. **Allow bypassing required checks**
265does not bypass it.
266
267It adds to **Required approvals**, which is checked first: an approval from
268a code owner also counts towards that number.
269
270```sh
271curl -X PATCH https://api.g1t.sh/repos/acme/api/settings \
272 -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \
273 -d '{"require_code_owner_review": true}'
274```
275
276## Errors
277
278g1t checks the file like a linter, and shows what is wrong with each line:
279
280- on the repository's **Settings → Branches and merging**, in the
281 **CODEOWNERS** panel, for the default branch;
282- on the file's own page, beside each line, when you open it in the code
283 view;
284- through the API, for any branch.
285
286| Kind | Example message |
287| --- | --- |
288| `too_large` | The CODEOWNERS file is larger than 3 MB, so none of it applies; make it smaller. |
289| `negation` | !docs/ starts with !, and negation is not supported; this line is skipped. Give the path a later rule with no owners instead. |
290| `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 ?. |
291| `bad_pattern` | / names no path; this line is skipped. Write a file or directory pattern, such as * or /docs/. |
292| `bad_owner` | nobody is not an owner; write @username, @workspace/team or an email address. |
293| `bad_section` | The approval count [0] must be a whole number from 1 to 10. |
294| `unknown_user` | @ana is not a g1t account. |
295| `unknown_team` | @acme/backend is not a team of acme. |
296| `unknown_email` | No g1t account has confirmed ana@example.com. |
297| `no_write_access` | @bo cannot write to this repository; code owners need the Write role or higher. |
298| `team_no_access` | @acme/docs has no access to this repository; give the team the Write role or higher. |
299
300A section header can also be refused with: "The section header has no
301closing ]; write it as [Name].", "The section header has no name; write it
302as [Name].", "The section name cannot contain [; write it as [Name].", "The
303approval count has no closing ]; write it as [Name][2]." or "Put a space
304between the section header and its owners."
305
306A line with an error in its pattern is skipped. An owner with an error is
307left out, and the rest of its line still applies.
308
309### The codeowners check
310
311A pull request that changes a CODEOWNERS file, in any of the
312[places g1t reads](#where-the-file-goes), gets a status on its head,
313`g1t / codeowners`: a failure such as `.github/CODEOWNERS has 2 errors`,
314or a success, `.github/CODEOWNERS has no errors`. **Details** opens the
315file as the pull request has it, with its errors by line. Make it a
316[required status check](/guides/pull-requests/#required-status-checks) to
317stop a broken file from merging.
318
319## Limits
320
321| | |
322| --- | --- |
323| File size | 3 MB. A larger file is ignored as a whole, with one error. |
324| Approvals per section | 1 to 10. |
325
326## Through the API
327
328| Route | MCP | What it does |
329| --- | --- | --- |
330| `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`. |
331| `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. |
332| `PATCH /repos/{owner}/{name}/settings` | `repository` `update_settings` | `require_code_owner_review`: `true` or `false`. Needs Admin. |
333
334```sh
335curl "https://api.g1t.sh/repos/acme/api/codeowners/errors?ref=main" \
336 -H "Authorization: Bearer $G1T_TOKEN"
337```
338
339```json
340{
341 "path": ".github/CODEOWNERS",
342 "ref": "main",
343 "size": 412,
344 "rules": 9,
345 "sections": ["Security", "Database"],
346 "errors": [
347 {
348 "line": 7,
349 "kind": "unknown_team",
350 "token": "@acme/backend",
351 "message": "@acme/backend is not a team of acme."
352 }
353 ]
354}
355```
356
357`path` is null when the repository has no CODEOWNERS file at that ref.
358Every route is in the [API reference](/reference/api/), and every action in
359[MCP tools](/reference/mcp/).