|
|
|
@ -8,11 +8,13 @@ import ( |
|
|
|
|
"context" |
|
|
|
|
"fmt" |
|
|
|
|
"io" |
|
|
|
|
"regexp" |
|
|
|
|
"strconv" |
|
|
|
|
"strings" |
|
|
|
|
|
|
|
|
|
"code.gitea.io/gitea/models/db" |
|
|
|
|
git_model "code.gitea.io/gitea/models/git" |
|
|
|
|
org_model "code.gitea.io/gitea/models/organization" |
|
|
|
|
pull_model "code.gitea.io/gitea/models/pull" |
|
|
|
|
repo_model "code.gitea.io/gitea/models/repo" |
|
|
|
|
user_model "code.gitea.io/gitea/models/user" |
|
|
|
@ -887,3 +889,222 @@ func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *gi |
|
|
|
|
func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { |
|
|
|
|
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullRequest) error { |
|
|
|
|
files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} |
|
|
|
|
|
|
|
|
|
if pr.IsWorkInProgress() { |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if err := pr.LoadBaseRepo(ctx); err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
repo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath()) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
defer repo.Close() |
|
|
|
|
|
|
|
|
|
branch, err := repo.GetDefaultBranch() |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
commit, err := repo.GetBranchCommit(branch) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var data string |
|
|
|
|
for _, file := range files { |
|
|
|
|
if blob, err := commit.GetBlobByPath(file); err == nil { |
|
|
|
|
data, err = blob.GetBlobContent() |
|
|
|
|
if err == nil { |
|
|
|
|
break |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
rules, _ := GetCodeOwnersFromContent(ctx, data) |
|
|
|
|
changedFiles, err := repo.GetFilesChangedBetween(git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) |
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
uniqUsers := make(map[int64]*user_model.User) |
|
|
|
|
uniqTeams := make(map[string]*org_model.Team) |
|
|
|
|
for _, rule := range rules { |
|
|
|
|
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 _, t := range rule.Teams { |
|
|
|
|
uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for _, u := range uniqUsers { |
|
|
|
|
if u.ID != pull.Poster.ID { |
|
|
|
|
if _, err := AddReviewRequest(pull, u, pull.Poster); err != nil { |
|
|
|
|
log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err) |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
for _, t := range uniqTeams { |
|
|
|
|
if _, err := AddTeamReviewRequest(pull, t, pull.Poster); err != nil { |
|
|
|
|
log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err) |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// GetCodeOwnersFromContent returns the code owners configuration
|
|
|
|
|
// Return empty slice if files missing
|
|
|
|
|
// Return warning messages on parsing errors
|
|
|
|
|
// We're trying to do the best we can when parsing a file.
|
|
|
|
|
// Invalid lines are skipped. Non-existent users and teams too.
|
|
|
|
|
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 |
|
|
|
|
Teams []*org_model.Team |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, []string) { |
|
|
|
|
var err error |
|
|
|
|
rule := &CodeOwnerRule{ |
|
|
|
|
Users: make([]*user_model.User, 0), |
|
|
|
|
Teams: make([]*org_model.Team, 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 { |
|
|
|
|
rule.Teams = append(rule.Teams, team) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} 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) && (len(rule.Teams) == 0) { |
|
|
|
|
warnings = append(warnings, "no users/groups 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 |
|
|
|
|
} |
|
|
|
|