Skip to content

Add CODEOWNERS feature #25060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 41 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
881b778
Add functions to auto-add hardcoded user reviewers to PRs
smoffat-et May 23, 2023
2f338e9
Add function to find CODEOWNERS file. Refactoring in issue service.
smoffat-et May 24, 2023
0b550fd
Owner teams found by {org}/{name} auto-added as reviewers
smoffat-et May 24, 2023
f4ace1e
Add checks to ensure eligibility of reviewers on codeowner auto-add
smoffat-et May 24, 2023
5e63d37
Add 3 MB CODEOWNERS file limit. Add 'docs/' as accepted location.
smoffat-et May 24, 2023
cdcde7b
Added Codeowners Parser file as codeowners.go
ihammsbc May 24, 2023
c280fa8
Added codeowners.go file which contains ParseCodeowners function.
ihammsbc May 25, 2023
f9a4e46
Add parser support for no-owners codeowners rule
smoffat-et May 25, 2023
33c1dc8
Fix small linting errors in codeowners.go
smoffat-et May 25, 2023
d334efb
Added unit tests and fixed a mapping bug
ihammsbc May 25, 2023
6e31092
Merge pull request 'Added unit tests and fixed a mapping bug' (#33) f…
ihammsbc May 25, 2023
a026059
Added syntax checks for users
ihammsbc May 25, 2023
9e02539
Add logging in issue service (that uses codeowners.go)
smoffat-et May 25, 2023
e6fc97c
Added Logging traces to both codeowners and codeowners_test file
ihammsbc May 25, 2023
c1a5940
Merge pull request 'CodeownersUnitTests' (#35) from CodeownersUnitTes…
ihammsbc May 25, 2023
9bd130c
Codeowners: request review only if has write perm
smoffat-et May 25, 2023
c131ac7
Merge branch 'main' of https://git.etogy.internal/etogy/gitea
smoffat-et May 26, 2023
47b87c3
Refactor code to use codeowners.go more
smoffat-et May 26, 2023
620155e
Refactor 3 MB size CODEOWNERS size limit check
smoffat-et May 26, 2023
f25a13c
Add shield icon for codeowner info on file inspect
smoffat-et May 26, 2023
13ae768
Refactor CODEOWNERS parser to expose line validation
smoffat-et May 26, 2023
b3feb60
Add CODEOWNERS validation messages when viewing file
smoffat-et May 26, 2023
9723941
Add CODEOWNER messages to edit view
smoffat-et May 30, 2023
75e37a9
Added comments to codeowners.go to try and improve clarity
ihammsbc May 30, 2023
952b373
Refactored IsValidCodeownersLine func for clarity
ihammsbc May 31, 2023
032f57e
Clarified some comments and refactored variables for clarity
ihammsbc May 31, 2023
eb867b5
Refactor codeowner shield into template. Add icon to diff lists.
smoffat-et May 31, 2023
ca23985
This branch is currently useless, but has the
ihammsbc May 31, 2023
80fbdd7
Localization testing
ihammsbc May 31, 2023
8b8bb9f
Major Refactoring for line validation
ihammsbc May 31, 2023
29b43fe
Fix CODEOWNERS localization and refactoring errors.
smoffat-et May 31, 2023
8a8e55d
Localized text for validation/ownership. Refactor codeowners to use v…
smoffat-et Jun 1, 2023
e190b75
Add codeowner shields to PR creation pages
smoffat-et Jun 1, 2023
c221a4d
Refactor codeowner .tmpl for better reuse
smoffat-et Jun 1, 2023
958c7c7
Add documentation page
smoffat-et Jun 2, 2023
cacb270
Fix linting/style errors. Add comments. Better error handling.
smoffat-et Jun 2, 2023
290b4ca
Merge branch 'main' into ihamm.RefactoringLineValidation
smoffat-et Jun 2, 2023
94063a8
Clarify/comment/clean up functions. Remove duplicate shield icon
smoffat-et Jun 2, 2023
1b898a1
Merge pull request 'ihamm.RefactoringLineValidation' (#53) from ihamm…
smoffat-et Jun 2, 2023
0a0ec43
Update CODEOWNERS docs page
smoffat-et Jun 2, 2023
0d66dfc
Prep for PR
smoffat-et Jun 2, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions docs/content/doc/usage/codeowners.en-us.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
date: "2023-06-02T16:00:00+00:00"
title: "Code Owners"
slug: "codeowners"
weight: 30
toc: false
draft: false
aliases:
- /en-us/codeowners
menu:
sidebar:
parent: "usage"
name: "Code Owners"
weight: 30
identifier: "codeowners"
---

# Code Owners
You can use a CODEOWNERS file to define individuals or teams that are responsible for code in a repository. Code owners are automatically requested for review when someone opens a pull request that modifies code that they own.

If a file has a code owner, you can see who the code owner is before you open a pull request. In a Gitea repository, you can browse to the file and hover over the shield icon to see a tool tip with code ownership details. You can also see this code ownership shield for individual files when opening a pull request, viewing a pull request's changed files, or viewing a commit.

## CODEOWNRS File Location
To use a CODEOWNERS file, create a new file called `CODEOWNERS` in the root, `docs/`, or `.gitea/` directory of the repository in the branch where you'd like to add the code owners. There should only be one such file. The first matching file is used to determine ownership and the rest are ignored.

Each CODEOWNERS file assigns the code owners for a single branch in the repository. Thus, you can assign different code owners for different branches.

For code owners to receive review requests, the CODEOWNERS file must be on the base branch of the pull request. For example, if you assign `@userA` as the code owner for *.js* files on the `feature-A` branch of your repository, `@userA` will receive review requests when a pull request with changes to *.js* files is opened between the head branch and `feature-A`.

## CODEOWNERS File Size
CODEOWNERS files must be under 3 MB in size. A CODEOWNERS file over this limit will not be loaded, which means that code owner information is not shown and the appropriate code owners will not be requested to review changes in a pull request.

To reduce the size of your CODEOWNERS file, consider using wildcard patterns to consolidate multiple entries into a single entry.

## CODEOWNERS Syntax
> **Warning**: There are some syntax rules for gitignore files that *do not work* in CODEOWNERS files:
> * Escaping a pattern starting with `#` using `\` so it is treated as a pattern and not a comment
> * Using `!` to negate a pattern
> * Using `[ ]` to define a character range

A CODEOWNERS file uses a pattern that follows most of the same rules used in [gitignore](https://git-scm.com/docs/gitignore#_pattern_format) files. The pattern is followed by one or more Gitea usernames or team names using the standard `@username` or `@org/team-name` format. Users must have explicit `write` access to the repository. Teams must also have explicit `write` access to the repository, even if the all the team's members already have access.

If you want to match two or more code owners with the same pattern, all the code owners must be on the same line. If the code owners are not on the same line, the pattern matches only the last mentioned code owner.

You can also refer to a user by their email address (for example, `[email protected]`). Note that if their email address changes after the CODEOWNERS file is created, they would fail to be identified when parsing the file.

CODEOWNERS paths are case sensitive, because Gitea uses a case sensitive file system. Since CODEOWNERS are evaluated by Gitea, even systems that are case insensitive (for example, macOS) must use paths and files that are cased correctly in the CODEOWNERS file.

If any line in your CODEOWNERS file contains invalid syntax or references a user or team that is ineligible, that line will be skipped. When you navigate to the CODEOWNERS file in your repository on Gitea, you can see any validation errors.

### Example CODEOWNERS file
```
# This is a comment.
# Each line is a file pattern followed by one or more owners.

# These owners will be the default owners for everything in the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for review when someone opens a pull request.
* @global-owner1 @global-owner2

# Order is important; the last matching pattern takes the most precedence. When someone opens a pull request
# that only modifies JS files, only @js-owner and not the global owner(s) will be requested for a review.
*.js @js-owner #This is an inline comment.

# You can also use email addresses if you prefer.
*.go [email protected]

# Teams can be specified as code owners as well. Teams should be identified in the format @org/team-name. Teams must have
# explicit write access to the repository. In this example, the octocats team in the octo-org organization owns all .txt files.
*.txt @octo-org/octocats

# In this example, @doctocat owns any files in the build/logs
# directory at the root of the repository and any of its subdirectories.
/build/logs/ @doctocat

# The `docs/*` pattern will match files like `docs/getting-started.md` but not further
# nested files like `docs/build-app/troubleshooting.md`.
docs/* [email protected]

# In this example, @octocat owns any file in an apps directory anywhere in your repository.
apps/ @octocat

# In this example, @doctocat owns any file in the `/docs` directory in the root of your repository and any of its subdirectories.
/docs/ @doctocat

# In this example, any change inside the `/scripts` directory will require approval from @doctocat or @octocat.
/scripts/ @doctocat @octocat

# In this example, @octocat owns any file in a `/logs` directory such as `/build/logs`, `/scripts/logs`, and
# `/deeply/nested/logs`. Any changes in a `/logs` directory will require approval from @octocat.
**/logs @octocat

# In this example, @octocat owns any file in the `/apps` directory in the root of your repository
# except for the `/apps/github` subdirectory, as its owners are left empty.
/apps/ @octocat
/apps/github
```
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ require (
xorm.io/xorm v1.3.3-0.20230219231735-056cecc97e9e
)

require (
github.com/MichaelTJones/walk v0.0.0-20161122175330-4748e29d5718 // indirect
github.com/mgutz/str v1.2.0 // indirect
)

require (
cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
Expand Down Expand Up @@ -292,6 +297,7 @@ require (
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/godo.v2 v2.0.9
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7Y
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/MichaelTJones/walk v0.0.0-20161122175330-4748e29d5718 h1:FSsoaa1q4jAaeiAUxf9H0PgFP7eA/UL6c3PdJH+nMN4=
github.com/MichaelTJones/walk v0.0.0-20161122175330-4748e29d5718/go.mod h1:VVwKsx9Dc8rNG55BWqogoJzGubjKnRoXdUvpGbWqeCc=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
Expand Down Expand Up @@ -872,6 +874,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/meilisearch/meilisearch-go v0.24.0 h1:GTP8LWZmkMYrGgX5BRZdkC2Txyp0mFYLzXYMlVV7cSQ=
github.com/meilisearch/meilisearch-go v0.24.0/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mholt/acmez v1.1.0 h1:IQ9CGHKOHokorxnffsqDvmmE30mDenO1lptYZ1AYkHY=
github.com/mholt/acmez v1.1.0/go.mod h1:zwo5+fbLLTowAX8o8ETfQzbDtwGEXnPhkmGdKIP+bgs=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
Expand Down Expand Up @@ -1764,6 +1768,8 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/godo.v2 v2.0.9 h1:jnbznTzXVk0JDKOxN3/LJLDPYJzIl0734y+Z0cEJb4A=
gopkg.in/godo.v2 v2.0.9/go.mod h1:wgvPPKLsWN0hPIJ4JyxvFGGbIW3fJMSrXhdvSuZ1z/8=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
Expand Down
7 changes: 7 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3441,3 +3441,10 @@ need_approval_desc = Need approval to run workflows for fork pull request.
type-1.display_name = Individual Project
type-2.display_name = Repository Project
type-3.display_name = Organization Project

[codeowners]
codeownership = Owned by %s (CODEOWNERS)
error_notice = This CODEOWNERS file contains errors:
validation_message_syntax = Error on line %d: make sure owners are formated like "@user", "[email protected]", or "@org-name/team-name"
validation_message_permissions_plural = Error on line %d: make sure %s exist and have "write" access to the repository
validation_message_permissions_singular = Error on line %d: make sure %s exists and has "write" access to the repository
42 changes: 42 additions & 0 deletions routers/web/repo/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/gitdiff"
issue_service "code.gitea.io/gitea/services/issue"
)

const (
Expand Down Expand Up @@ -62,6 +63,39 @@ func setCompareContext(ctx *context.Context, before, head *git.Commit, headOwner
setPathsCompareContext(ctx, before, head, headOwner, headName)
setImageCompareContext(ctx)
setCsvCompareContext(ctx)
err := setCodeownersContext(ctx, ctx.Repo.CommitID, before.ID.String(), head.ID.String())
if err != nil {
log.Warn("setCodeownersContext: %v", err)
}
}

// setCodeownersContext gets a map of file paths to their code owners using the CODEOWNERS in the base branch and puts it in the context data
func setCodeownersContext(ctx *context.Context, baseCommitID, beforeCommitID, headCommitID string) error {
gitRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
if err != nil {
return err
}
defer gitRepo.Close()

mapping := make(map[string]string)
baseCommit, err := gitRepo.GetCommit(baseCommitID)
if err == nil {
codeownersContents, err := issue_service.GetCodeownersFileContents(ctx, baseCommit, gitRepo)
if err == nil && codeownersContents != nil && len(codeownersContents) > 0 {
changedFiles, err := gitRepo.GetFilesChangedBetween(beforeCommitID, headCommitID)
if err == nil {
for _, filePath := range changedFiles {
codeowners, err := GetFileCodeowners(ctx, filePath, codeownersContents)
if err == nil && len(codeowners) > 0 {
mapping[filePath] = ctx.Locale.Tr("codeowners.codeownership", strings.Join(codeowners, ", "))
}
}
}
}
}

ctx.Data["CodeownersFileMap"] = mapping
return nil
}

// SourceCommitURL creates a relative URL for a commit in the given repository
Expand Down Expand Up @@ -777,6 +811,14 @@ func CompareDiff(ctx *context.Context) {
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
afterCommitID := ctx.Data["AfterCommitID"].(string)

// Want to display codeownership of changed files if there are any to display
if ctx.Data["PageIsComparePull"] == true && !nothingToCompare {
err := setCodeownersContext(ctx, ci.CompareInfo.BaseCommitID, beforeCommitID, afterCommitID)
if err != nil {
log.Warn("setCodeownersContext: %v", err)
}
}

separator := "..."
if ci.DirectComparison {
separator = ".."
Expand Down
5 changes: 5 additions & 0 deletions routers/web/repo/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["Editorconfig"] = GetEditorConfig(ctx, treePath)

err := SetCodeownerValidationInfo(ctx)
if err != nil {
log.Warn("GetCodeownerValidationInfo: %v", err)
}

ctx.HTML(http.StatusOK, tplEditFile)
}

Expand Down
85 changes: 85 additions & 0 deletions routers/web/repo/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,11 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
ctx.Data["IsDisplayingSource"] = isDisplayingSource
ctx.Data["IsDisplayingRendered"] = isDisplayingRendered

err = SetCodeownersOwnershipInfo(ctx)
if err != nil {
log.Warn("SetCodeownersOwnershipInfo: %v", err)
}

isTextSource := fInfo.isTextFile || isDisplayingSource
ctx.Data["IsTextSource"] = isTextSource
if isTextSource {
Expand Down Expand Up @@ -582,6 +587,86 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
} else if !ctx.Repo.CanWriteToBranch(ctx.Doer, ctx.Repo.BranchName) {
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
}

if entry.Name() == "CODEOWNERS" {
err := SetCodeownerValidationInfo(ctx)
if err != nil {
log.Warn("GetCodeownerValidationInfo: %v", err)
}
}
}

// SetCodeownerValidationInfo gets the validation information for the current file (assumes is "CODEOWNERS") and sets the context data accordingly
func SetCodeownerValidationInfo(ctx *context.Context) error {
contentsBytes, err := issue_service.GetCodeownersFileContents(ctx, ctx.Repo.Commit, ctx.Repo.GitRepo)
if err != nil {
return err
}

_, _, validationErrors, err := issue_service.ParseCodeowners(ctx, ctx.Repo.Repository, ctx.Doer, []string{}, contentsBytes)
if err != nil {
return err
}

if len(validationErrors) > 0 {
ctx.Data["CodeownersValidationNotice"] = ctx.Locale.Tr("codeowners.error_notice")
validationMessages := []string{}
for lineNum, err := range validationErrors {
var message string
switch problem := err.(type) {
default:
return fmt.Errorf("CODEOWNERS validation error was not of type InvalidSyntaxError or ExistenceAndPermissionError [validation_error: %v]", problem)
case *issue_service.InvalidSyntaxError:
message = ctx.Locale.Tr("codeowners.validation_message_syntax", lineNum)
case *issue_service.ExistenceAndPermissionError:
if len(problem.Owners()) > 1 {
message = ctx.Locale.Tr("codeowners.validation_message_permissions_plural", lineNum, strings.Join(problem.Owners(), ", "))
} else {
message = ctx.Locale.Tr("codeowners.validation_message_permissions_singular", lineNum, strings.Join(problem.Owners(), ", "))
}
}
validationMessages = append(validationMessages, message)
}
ctx.Data["CodeownersValidationErrors"] = validationMessages
}

return nil
}

// SetCodeownersOwnershipInfo gets the ownership information for the current file and sets the context data accordingly
func SetCodeownersOwnershipInfo(ctx *context.Context) error {
gitRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
if err != nil {
return err
}
defer gitRepo.Close()

commit, err := gitRepo.GetCommit(ctx.Repo.CommitID)
if err == nil {
codeownersContents, err := issue_service.GetCodeownersFileContents(ctx, commit, gitRepo)
if err == nil && codeownersContents != nil && len(codeownersContents) > 0 {
codeowners, err := GetFileCodeowners(ctx, ctx.Repo.TreePath, codeownersContents)
if err == nil && len(codeowners) > 0 {
ctx.Data["Codeowners"] = ctx.Locale.Tr("codeowners.codeownership", strings.Join(codeowners, ", "))
}
}
}

return nil
}

// GetFileCodeowners gets the codeowners for the given file path and CODEOWNERS file contents
func GetFileCodeowners(ctx *context.Context, filePath string, codeownersContents []byte) (codeowners []string, err error) {
userOwners, teamOwners, _, err := issue_service.ParseCodeowners(ctx, ctx.Repo.Repository, ctx.Doer, []string{filePath}, codeownersContents)
if err == nil {
for _, userOwner := range userOwners {
codeowners = append(codeowners, userOwner.Name)
}
for _, teamOwner := range teamOwners {
codeowners = append(codeowners, teamOwner.Name)
}
}
return codeowners, err
}

func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output string, err error) {
Expand Down
Loading