-
-
Notifications
You must be signed in to change notification settings - Fork 5.9k
Add codeowners feature #24910
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
Merged
Merged
Add codeowners feature #24910
Changes from 9 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
882d97a
Add codeowners feature
cl-bvl c8eb310
Merge branch 'main' into feature/code_owners
cl-bvl 56803ca
Fix bugs
cl-bvl e6bdaa2
Fix
cl-bvl d68546a
Merge branch 'main' into feature/code_owners
cl-bvl b7ec465
Update modules/git/repo_show.go
cl-bvl 7136afe
Fixes
cl-bvl c7d71a8
Fix doc
cl-bvl d3d4732
Add file verify
cl-bvl dba89f1
Revert
cl-bvl b679754
Add WIP support
cl-bvl 8286142
Merge branch 'main' into feature/code_owners
cl-bvl a801f70
Fixes
cl-bvl ad0a3a5
Fixes
cl-bvl 291aca7
Merge branch 'main' into feature/code_owners
cl-bvl 76b86ef
Add tests
cl-bvl 1b80b88
Merge branch 'feature/code_owners' of github.com:WinnerSoftLab/gitea …
cl-bvl a1364f3
Merge branch 'feature/code_owners' of github.com:WinnerSoftLab/gitea …
lunny 7ddd654
Fix lint
lunny ae05469
Merge pull request #1 from lunny/WinnerSoftLab-feature/code_owners
cl-bvl 81e6616
Merge branch 'main' into feature/code_owners
cl-bvl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
--- | ||
date: "2023-05-24T16:00:00+00:00" | ||
title: "Code Owners" | ||
slug: "code-owners" | ||
weight: 30 | ||
toc: false | ||
draft: false | ||
aliases: | ||
- /en-us/code-owners | ||
menu: | ||
sidebar: | ||
parent: "usage" | ||
name: "Code Owners" | ||
weight: 30 | ||
identifier: "code-owners" | ||
--- | ||
|
||
# Code Owners | ||
|
||
Gitea maintains code owner files. It looks for it in the following locations in this order: | ||
|
||
- `./CODEOWNERS` | ||
- `./docs/CODEOWNERS` | ||
- `./.gitea/CODEOWNERS` | ||
|
||
And stops at the first found file. | ||
|
||
File format: `<regexp rule> <@user or @org/team> [@user or @org/team]...` | ||
|
||
Regexp specified in golang Regex format. | ||
Regexp can start with `!` for negative rules - match all files except specified. | ||
|
||
Example file: | ||
|
||
``` | ||
.*\\.go @user1 @user2 # This is comment | ||
|
||
# Comment too | ||
# You can assigning code owning for users or teams | ||
frontend/src/.*\\.js @org1/team1 @org1/team2 @user3 | ||
|
||
# You can use negative pattern | ||
!frontend/src/.* @org1/team3 @user5 | ||
|
||
# You can use power of go regexp | ||
docs/(aws|google|azure)/[^/]*\\.(md|txt) @user8 @org1/team4 | ||
!/assets/.*\\.(bin|exe|msi) @user9 | ||
``` | ||
|
||
### Escaping | ||
|
||
You can escape characters `#`, ` ` (space) and `\` with `\`, like: | ||
|
||
``` | ||
dir/with\#hashtag @user1 | ||
path\ with\ space @user2 | ||
path/with\\backslash @user3 | ||
``` | ||
|
||
Some character (`.+*?()|[]{}^$\`) should be escaped with `\\` inside regexp, like: | ||
|
||
``` | ||
path/\\.with\\.dots | ||
path/with\\+plus | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ import ( | |
"code.gitea.io/gitea/models/db" | ||
git_model "code.gitea.io/gitea/models/git" | ||
issues_model "code.gitea.io/gitea/models/issues" | ||
org_model "code.gitea.io/gitea/models/organization" | ||
repo_model "code.gitea.io/gitea/models/repo" | ||
user_model "code.gitea.io/gitea/models/user" | ||
"code.gitea.io/gitea/modules/container" | ||
|
@@ -123,6 +124,32 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issu | |
} | ||
|
||
_, _ = issue_service.CreateComment(ctx, ops) | ||
|
||
if coRules, _, err := GetCodeOwners(ctx, repo, repo.DefaultBranch); err == nil && len(coRules) > 0 { | ||
changedFiles, err := baseGitRepo.GetFilesChangedBetween(git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
uniqUsers := make(map[int64]*user_model.User) | ||
for _, rule := range coRules { | ||
for _, f := range changedFiles { | ||
if (rule.Rule.MatchString(f) && !rule.Negative) || (!rule.Rule.MatchString(f) && rule.Negative) { | ||
for _, u := range rule.Users { | ||
uniqUsers[u.ID] = u | ||
} | ||
} | ||
} | ||
} | ||
|
||
for _, u := range uniqUsers { | ||
if u.ID != pull.Poster.ID { | ||
if _, err := issues_model.AddReviewRequest(pull, u, pull.Poster); err != nil { | ||
log.Warn("Failed add assignee user: %s to PR review: %s#%d", u.Name, pr.BaseRepo.Name, pr.ID) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
|
@@ -838,3 +865,175 @@ func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, br | |
} | ||
return baseCommit.HasPreviousCommit(headCommit.ID) | ||
} | ||
|
||
// GetCodeOwners returns the code owners configuration | ||
// Return empty slice if files missing | ||
// Return error on file system errors | ||
// We're trying to do the best we can when parsing a file. | ||
// Invalid lines are skipped. Non-existent users and groups too. | ||
func GetCodeOwners(ctx context.Context, repo *repo_model.Repository, branch string) ([]*CodeOwnerRule, []string, error) { | ||
files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} | ||
techknowlogick marked this conversation as resolved.
Show resolved
Hide resolved
|
||
gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath()) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
defer closer.Close() | ||
|
||
if !gitRepo.IsBranchExist(branch) { | ||
return nil, nil, &git.ErrBranchNotExist{Name: branch} | ||
} | ||
|
||
commit, err := gitRepo.GetBranchCommit(branch) | ||
|
||
var data string | ||
for _, file := range files { | ||
if blob, err := commit.GetBlobByPath(file); err == nil { | ||
data, err = blob.GetBlobContent() | ||
if err == nil { | ||
break | ||
} | ||
} | ||
} | ||
|
||
rules, warnings := GetCodeOwnersFromContent(ctx, data) | ||
lunny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return rules, warnings, nil | ||
} | ||
|
||
func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRule, []string) { | ||
if len(data) == 0 { | ||
return nil, nil | ||
} | ||
|
||
rules := make([]*CodeOwnerRule, 0) | ||
lines := strings.Split(data, "\n") | ||
warnings := make([]string, 0) | ||
|
||
for i, line := range lines { | ||
tokens := tokenizeCodeOwnersLine(line) | ||
if len(tokens) == 0 { | ||
continue | ||
} else if len(tokens) < 2 { | ||
warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1)) | ||
continue | ||
} | ||
rule, wr := parseCodeOwnersLine(ctx, tokens) | ||
for _, w := range wr { | ||
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w)) | ||
} | ||
if rule == nil { | ||
continue | ||
} | ||
|
||
rules = append(rules, rule) | ||
} | ||
|
||
return rules, warnings | ||
} | ||
|
||
type CodeOwnerRule struct { | ||
Rule *regexp.Regexp | ||
Negative bool | ||
Users []*user_model.User | ||
} | ||
|
||
func parseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, []string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO it needs some tests to cover the code, to make sure the logic won't be broken by future changes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added test for file parser |
||
var err error | ||
rule := &CodeOwnerRule{ | ||
Users: make([]*user_model.User, 0), | ||
Negative: strings.HasPrefix(tokens[0], "!"), | ||
} | ||
|
||
warnings := make([]string, 0) | ||
|
||
rule.Rule, err = regexp.Compile(fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!"))) | ||
if err != nil { | ||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err)) | ||
return nil, warnings | ||
} | ||
|
||
for _, user := range tokens[1:] { | ||
user = strings.TrimPrefix(user, "@") | ||
|
||
// Only @org/team can contain slashes | ||
if strings.Contains(user, "/") { | ||
s := strings.Split(user, "/") | ||
if len(s) != 2 { | ||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner group: %s", user)) | ||
continue | ||
} | ||
orgName := s[0] | ||
teamName := s[1] | ||
|
||
org, err := org_model.GetOrgByName(ctx, orgName) | ||
if err != nil { | ||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner organization: %s", user)) | ||
continue | ||
} | ||
teams, err := org.LoadTeams() | ||
if err != nil { | ||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner team: %s", user)) | ||
continue | ||
} | ||
|
||
for _, team := range teams { | ||
if team.Name == teamName { | ||
if err := team.LoadMembers(ctx); err != nil { | ||
continue | ||
} | ||
rule.Users = append(rule.Users, team.Members...) | ||
} | ||
} | ||
} else { | ||
u, err := user_model.GetUserByName(ctx, user) | ||
if err != nil { | ||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner user: %s", user)) | ||
continue | ||
} | ||
rule.Users = append(rule.Users, u) | ||
} | ||
} | ||
|
||
if len(rule.Users) == 0 { | ||
warnings = append(warnings, "no users matched") | ||
return nil, warnings | ||
} | ||
|
||
return rule, warnings | ||
} | ||
|
||
func tokenizeCodeOwnersLine(line string) []string { | ||
if len(line) == 0 { | ||
return nil | ||
} | ||
|
||
line = strings.TrimSpace(line) | ||
line = strings.ReplaceAll(line, "\t", " ") | ||
|
||
tokens := make([]string, 0) | ||
|
||
escape := false | ||
token := "" | ||
for _, char := range line { | ||
if escape { | ||
token += string(char) | ||
escape = false | ||
} else if string(char) == "\\" { | ||
escape = true | ||
} else if string(char) == "#" { | ||
break | ||
} else if string(char) == " " { | ||
if len(token) > 0 { | ||
tokens = append(tokens, token) | ||
token = "" | ||
} | ||
} else { | ||
token += string(char) | ||
} | ||
} | ||
|
||
if len(token) > 0 { | ||
tokens = append(tokens, token) | ||
} | ||
|
||
return tokens | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.