| 1 | --- |
| 2 | title: CODEOWNERS |
| 3 | description: 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 | |
| 6 | A **CODEOWNERS** file says who owns which files in a repository. When a |
| 7 | pull request changes files, g1t asks their owners to review it, shows on |
| 8 | the pull request whose approval is still needed, and, if the repository |
| 9 | requires 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 | |
| 27 | Put the file in one of these places on the repository's default branch. |
| 28 | g1t 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 | |
| 38 | So a repository you bring to g1t with its file already at |
| 39 | `.github/CODEOWNERS` or `.gitlab/CODEOWNERS` works as it is. The name is |
| 40 | case-sensitive: `codeowners` is not read, and neither is a file anywhere |
| 41 | else, such as `src/CODEOWNERS`. |
| 42 | |
| 43 | g1t always reads the file from the branch a pull request merges into, not |
| 44 | from the pull request. A change to the file takes effect once it is merged. |
| 45 | |
| 46 | ## Rules |
| 47 | |
| 48 | Each line is a **pattern**, then the **owners** of the paths it matches, |
| 49 | separated by spaces: |
| 50 | |
| 51 | ```text |
| 52 | /api/ @ana @acme/backend |
| 53 | ``` |
| 54 | |
| 55 | For each file, **the last rule that matches it wins**. Put broad rules |
| 56 | first 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 | |
| 73 | The order matters: written the other way round, with `*` last, `@acme/engineering` |
| 74 | would own everything. |
| 75 | |
| 76 | ### No owners |
| 77 | |
| 78 | A rule with no owners says the paths it matches have none. Their changes |
| 79 | need no code owner's review: |
| 80 | |
| 81 | ```text |
| 82 | * @acme/engineering |
| 83 | /vendor/ |
| 84 | package-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 | |
| 97 | Blank lines, a byte order mark and Windows line ends (`\r\n`) are all |
| 98 | fine. |
| 99 | |
| 100 | ## Patterns |
| 101 | |
| 102 | Paths 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 | |
| 121 | In 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 | |
| 135 | Negation (`!`) and character ranges (`[a-z]`) are not part of the format. |
| 136 | A line that uses them is skipped and shown as an [error](#errors). To give |
| 137 | part 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 | |
| 148 | A file written for another host may name teams under an organization that |
| 149 | is not a g1t workspace, such as `@acme-corp/backend`. g1t reads those as |
| 150 | teams 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 |
| 152 | different g1t workspace never owns anything here. |
| 153 | |
| 154 | An owner that does not resolve, or cannot write to the repository, owns |
| 155 | nothing, and is shown as an [error](#errors). A rule left with no owner |
| 156 | that resolves asks for no review. |
| 157 | |
| 158 | ## Sections |
| 159 | |
| 160 | A line such as `[Docs]` starts a **section**. The rules after it belong to |
| 161 | it, until the next section header. Rules before the first header are in the |
| 162 | default section. |
| 163 | |
| 164 | Each section applies its own last match. One file can need reviews from |
| 165 | more than one section: |
| 166 | |
| 167 | ```text |
| 168 | *.rb @ruby |
| 169 | |
| 170 | [Security] |
| 171 | config/secrets/ @acme/security |
| 172 | |
| 173 | [Database][2] @acme/data |
| 174 | db/ |
| 175 | db/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 | |
| 196 | Section names are compared without regard to case. Two headers with the |
| 197 | same name are one section: the first spelling is kept, and a later |
| 198 | header's approval count and default owners replace the earlier ones when |
| 199 | it gives them. A later `^` makes the section optional. |
| 200 | |
| 201 | In a section with default owners, a rule with no owners gets the |
| 202 | defaults. To say some paths have no owners there, put them in a section |
| 203 | without defaults. |
| 204 | |
| 205 | A header that cannot be read is an error, and the rules after it stay in |
| 206 | the section before it. |
| 207 | |
| 208 | ## Review requests |
| 209 | |
| 210 | When a pull request is opened, marked ready, or pushed to, g1t works out |
| 211 | who 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 | |
| 222 | The inbox tells them **acme/api#42 changes files you own**, or **acme/api#42 |
| 223 | changes files @acme/backend owns**, with the reason `review_requested`. |
| 224 | Webhooks get `pull.review_requested` with `data.code_owners` set to `true`. |
| 225 | |
| 226 | ## On the pull request |
| 227 | |
| 228 | A pull request whose target has a CODEOWNERS file shows **Code owners**: |
| 229 | which rule owns which changed files, each with its section, its owners, |
| 230 | how many approvals it needs, who has approved, and who has asked for |
| 231 | changes. A rule that is satisfied, or optional, is marked so. |
| 232 | |
| 233 | When the repository requires code owners' approval, the merge box lists |
| 234 | what 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 | |
| 243 | Someone with the Maintain role or higher turns it on under the |
| 244 | repository's **Settings → Branches and merging**, in **Branch protection**: |
| 245 | **Require review from code owners**. It is off by default. |
| 246 | |
| 247 | With it on, a pull request merges only when every rule that owns a changed |
| 248 | file has the approvals its section asks for, from its owners, and no code |
| 249 | owner has asked for changes. It is worked out again at the moment of the |
| 250 | merge, from the file on the target branch as it is then. |
| 251 | |
| 252 | What 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 | |
| 262 | It holds wherever a pull request merges: the merge button, |
| 263 | [`merge_pull_request`](/reference/api/pull-requests/merge-pull-request/), |
| 264 | a g1t agent's [automatic merge](/guides/working-with-g1t/#merging-automatically), |
| 265 | and the [merge queue](/guides/merge-queue/). It holds pull requests g1t |
| 266 | opens by itself too, such as [security updates](/guides/security/), which |
| 267 | then need a person who owns the files. **Allow bypassing required checks** |
| 268 | does not bypass it. |
| 269 | |
| 270 | It adds to **Required approvals**, which is checked first: an approval from |
| 271 | a code owner also counts towards that number. |
| 272 | |
| 273 | ```sh |
| 274 | curl -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 | |
| 281 | g1t 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 | |
| 303 | A section header can also be refused with: "The section header has no |
| 304 | closing ]; write it as [Name].", "The section header has no name; write it |
| 305 | as [Name].", "The section name cannot contain [; write it as [Name].", "The |
| 306 | approval count has no closing ]; write it as [Name][2]." or "Put a space |
| 307 | between the section header and its owners." |
| 308 | |
| 309 | A line with an error in its pattern is skipped. An owner with an error is |
| 310 | left out, and the rest of its line still applies. |
| 311 | |
| 312 | ### The codeowners check |
| 313 | |
| 314 | A 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`, |
| 317 | or a success, `.github/CODEOWNERS has no errors`. **Details** opens the |
| 318 | file 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 |
| 320 | stop 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 |
| 338 | curl "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. |
| 361 | Every route is in the [API reference](/reference/api/), and every action in |
| 362 | [MCP tools](/reference/mcp/). |