From 882d97a26e0194fcccbd290ed5ed0126f337e6e8 Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Wed, 24 May 2023 15:22:31 +0300 Subject: [PATCH 01/13] Add codeowners feature --- models/repo/repo.go | 154 ++++++++++++++++++++++++++++++++++++++- modules/git/repo_show.go | 20 +++++ services/pull/pull.go | 25 +++++++ 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 modules/git/repo_show.go diff --git a/models/repo/repo.go b/models/repo/repo.go index d3e6daa95b53a..c95d532939dc0 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -10,12 +10,15 @@ import ( "net" "net/url" "path/filepath" + "regexp" "strconv" "strings" "code.gitea.io/gitea/models/db" + org_model "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" @@ -382,7 +385,7 @@ func (repo *Repository) LoadOwner(ctx context.Context) (err error) { return err } -// MustOwner always returns a valid *user_model.User object to avoid +// MustOwner always returns a valid *user_model.Users object to avoid // conceptually impossible error handling. // It creates a fake object that contains error details // when error occurs. @@ -762,6 +765,55 @@ func (repo *Repository) TemplateRepo() *Repository { return repo } +// GetCodeOwners returns the code owners configuration +// Return empty slice if files missing +// Return error on file parsion errors +func (repo *Repository) GetCodeOwners(ctx context.Context, branch string) ([]*CodeOwnerRule, error) { + files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS", ".github/CODEOWNERS"} + gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath()) + if err != nil { + return nil, err + } + defer closer.Close() + + if !gitRepo.IsBranchExist(branch) { + return nil, &git.ErrBranchNotExist{Name: branch} + } + + var data []byte + for _, file := range files { + data, err = gitRepo.GetFileContent(branch, file) + if err == nil { + break + } + } + + if len(data) == 0 { + return nil, nil + } + + rules := make([]*CodeOwnerRule, 0) + lines := strings.Split(string(data), "\n") + + for _, line := range lines { + tokens := tokenizeCodeOwnersLine(line) + if len(tokens) == 0 { + continue + } else if len(tokens) != 2 { + log.Info("Incorrect codeowner line: %s", line) + continue + } + rule := parseCodeOwnersLine(ctx, tokens) + if rule == nil { + continue + } + + rules = append(rules, rule) + } + + return rules, nil +} + type CountRepositoryOptions struct { OwnerID int64 Private util.OptionalBool @@ -832,3 +884,103 @@ func UpdateRepositoryOwnerName(ctx context.Context, oldUserName, newUserName str } return nil } + +type CodeOwnerRule struct { + Rule *regexp.Regexp + Negative bool + Users []*user_model.User +} + +func parseCodeOwnersLine(ctx context.Context, tokens []string) *CodeOwnerRule { + var err error + rule := &CodeOwnerRule{ + Users: make([]*user_model.User, 0), + Negative: strings.HasPrefix(tokens[0], "!"), + } + + rule.Rule, err = regexp.Compile(fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!"))) + if err != nil { + log.Info("Incorrect codeowner regexp: %s, error: %s", tokens[0], err) + return nil + } + + 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 { + log.Info("Incorrect codeowner group: %s", user) + continue + } + orgName := s[0] + teamName := s[1] + + org, err := org_model.GetOrgByName(ctx, orgName) + if err != nil { + log.Info("Incorrect codeowner org name: %s", user) + } + teams, err := org.LoadTeams() + if err != nil { + log.Info("Incorrect codeowner team name: %s", user) + } + + 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 { + continue + } + rule.Users = append(rule.Users, u) + } + } + + if len(rule.Users) == 0 { + return nil + } + + return rule +} + +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) == " " && len(token) > 0 { + tokens = append(tokens, token) + token = "" + } else { + token += string(char) + } + } + + if len(token) > 0 { + tokens = append(tokens, token) + } + + return tokens +} diff --git a/modules/git/repo_show.go b/modules/git/repo_show.go new file mode 100644 index 0000000000000..21fede67260a1 --- /dev/null +++ b/modules/git/repo_show.go @@ -0,0 +1,20 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "fmt" +) + +// GetRefs returns all references of the repository. +func (repo *Repository) GetFileContent(rev, path string) ([]byte, error) { + cmd := NewCommand(repo.Ctx, "show") + cmd.AddDynamicArguments(fmt.Sprintf("%s:%s", rev, path)) + stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + + return stdout, nil +} diff --git a/services/pull/pull.go b/services/pull/pull.go index 8f2befa8ffc6c..e83a72ed4919b 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -123,6 +123,31 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issu } _, _ = issue_service.CreateComment(ctx, ops) + + if coRules, err := repo.GetCodeOwners(ctx, pr.BaseBranch); err == nil { + 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 _, err := issues_model.AddReviewRequest(pull, pull.Poster, u); err != nil { + log.Warn("Failed add assignee user: %s to PR review: %s#%d", u.Name, pr.BaseRepo.Name, pr.ID) + } + } + } } return nil From 56803ca90ff32cfc78583373ab8801d54ce7bbf3 Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Wed, 24 May 2023 17:52:05 +0300 Subject: [PATCH 02/13] Fix bugs --- docs/content/doc/usage/code-owners.en-us.md | 60 +++++++ models/repo/repo.go | 152 ------------------ modules/git/repo_show.go | 2 +- services/pull/pull.go | 164 +++++++++++++++++++- 4 files changed, 222 insertions(+), 156 deletions(-) create mode 100644 docs/content/doc/usage/code-owners.en-us.md diff --git a/docs/content/doc/usage/code-owners.en-us.md b/docs/content/doc/usage/code-owners.en-us.md new file mode 100644 index 0000000000000..0cdb5ba82e329 --- /dev/null +++ b/docs/content/doc/usage/code-owners.en-us.md @@ -0,0 +1,60 @@ +--- +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. + +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 + +# 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 +``` diff --git a/models/repo/repo.go b/models/repo/repo.go index c95d532939dc0..42945bc624d6b 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -10,15 +10,12 @@ import ( "net" "net/url" "path/filepath" - "regexp" "strconv" "strings" "code.gitea.io/gitea/models/db" - org_model "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" @@ -765,55 +762,6 @@ func (repo *Repository) TemplateRepo() *Repository { return repo } -// GetCodeOwners returns the code owners configuration -// Return empty slice if files missing -// Return error on file parsion errors -func (repo *Repository) GetCodeOwners(ctx context.Context, branch string) ([]*CodeOwnerRule, error) { - files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS", ".github/CODEOWNERS"} - gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath()) - if err != nil { - return nil, err - } - defer closer.Close() - - if !gitRepo.IsBranchExist(branch) { - return nil, &git.ErrBranchNotExist{Name: branch} - } - - var data []byte - for _, file := range files { - data, err = gitRepo.GetFileContent(branch, file) - if err == nil { - break - } - } - - if len(data) == 0 { - return nil, nil - } - - rules := make([]*CodeOwnerRule, 0) - lines := strings.Split(string(data), "\n") - - for _, line := range lines { - tokens := tokenizeCodeOwnersLine(line) - if len(tokens) == 0 { - continue - } else if len(tokens) != 2 { - log.Info("Incorrect codeowner line: %s", line) - continue - } - rule := parseCodeOwnersLine(ctx, tokens) - if rule == nil { - continue - } - - rules = append(rules, rule) - } - - return rules, nil -} - type CountRepositoryOptions struct { OwnerID int64 Private util.OptionalBool @@ -884,103 +832,3 @@ func UpdateRepositoryOwnerName(ctx context.Context, oldUserName, newUserName str } return nil } - -type CodeOwnerRule struct { - Rule *regexp.Regexp - Negative bool - Users []*user_model.User -} - -func parseCodeOwnersLine(ctx context.Context, tokens []string) *CodeOwnerRule { - var err error - rule := &CodeOwnerRule{ - Users: make([]*user_model.User, 0), - Negative: strings.HasPrefix(tokens[0], "!"), - } - - rule.Rule, err = regexp.Compile(fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!"))) - if err != nil { - log.Info("Incorrect codeowner regexp: %s, error: %s", tokens[0], err) - return nil - } - - 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 { - log.Info("Incorrect codeowner group: %s", user) - continue - } - orgName := s[0] - teamName := s[1] - - org, err := org_model.GetOrgByName(ctx, orgName) - if err != nil { - log.Info("Incorrect codeowner org name: %s", user) - } - teams, err := org.LoadTeams() - if err != nil { - log.Info("Incorrect codeowner team name: %s", user) - } - - 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 { - continue - } - rule.Users = append(rule.Users, u) - } - } - - if len(rule.Users) == 0 { - return nil - } - - return rule -} - -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) == " " && len(token) > 0 { - tokens = append(tokens, token) - token = "" - } else { - token += string(char) - } - } - - if len(token) > 0 { - tokens = append(tokens, token) - } - - return tokens -} diff --git a/modules/git/repo_show.go b/modules/git/repo_show.go index 21fede67260a1..c1ae4fd99e305 100644 --- a/modules/git/repo_show.go +++ b/modules/git/repo_show.go @@ -7,7 +7,7 @@ import ( "fmt" ) -// GetRefs returns all references of the repository. +// GetFileContent returns file content for given revision. func (repo *Repository) GetFileContent(rev, path string) ([]byte, error) { cmd := NewCommand(repo.Ctx, "show") cmd.AddDynamicArguments(fmt.Sprintf("%s:%s", rev, path)) diff --git a/services/pull/pull.go b/services/pull/pull.go index e83a72ed4919b..884c738139e1f 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -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" @@ -124,7 +125,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issu _, _ = issue_service.CreateComment(ctx, ops) - if coRules, err := repo.GetCodeOwners(ctx, pr.BaseBranch); err == nil { + if coRules, err := GetCodeOwners(ctx, repo, pr.BaseBranch); err == nil { changedFiles, err := baseGitRepo.GetFilesChangedBetween(git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) if err != nil { return err @@ -143,8 +144,10 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issu } for _, u := range uniqUsers { - if _, err := issues_model.AddReviewRequest(pull, pull.Poster, u); err != nil { - log.Warn("Failed add assignee user: %s to PR review: %s#%d", u.Name, pr.BaseRepo.Name, pr.ID) + 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) + } } } } @@ -863,3 +866,158 @@ 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, error) { + files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} + gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath()) + if err != nil { + return nil, err + } + defer closer.Close() + + if !gitRepo.IsBranchExist(branch) { + return nil, &git.ErrBranchNotExist{Name: branch} + } + + var data []byte + for _, file := range files { + data, err = gitRepo.GetFileContent(branch, file) + if err == nil { + break + } + } + + if len(data) == 0 { + return nil, nil + } + + rules := make([]*CodeOwnerRule, 0) + lines := strings.Split(string(data), "\n") + + for _, line := range lines { + tokens := tokenizeCodeOwnersLine(line) + if len(tokens) == 0 { + continue + } else if len(tokens) < 2 { + log.Info("Incorrect codeowner line: %s", line) + continue + } + rule := parseCodeOwnersLine(ctx, tokens) + if rule == nil { + continue + } + + rules = append(rules, rule) + } + + return rules, nil +} + +type CodeOwnerRule struct { + Rule *regexp.Regexp + Negative bool + Users []*user_model.User +} + +func parseCodeOwnersLine(ctx context.Context, tokens []string) *CodeOwnerRule { + var err error + rule := &CodeOwnerRule{ + Users: make([]*user_model.User, 0), + Negative: strings.HasPrefix(tokens[0], "!"), + } + + rule.Rule, err = regexp.Compile(fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!"))) + if err != nil { + log.Info("Incorrect codeowner regexp: %s, error: %s", tokens[0], err) + return nil + } + + 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 { + log.Info("Incorrect codeowner group: %s", user) + continue + } + orgName := s[0] + teamName := s[1] + + org, err := org_model.GetOrgByName(ctx, orgName) + if err != nil { + log.Info("Incorrect codeowner org name: %s", user) + } + teams, err := org.LoadTeams() + if err != nil { + log.Info("Incorrect codeowner team name: %s", user) + } + + 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 { + continue + } + rule.Users = append(rule.Users, u) + } + } + + if len(rule.Users) == 0 { + return nil + } + + return rule +} + +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) == "/" { + // token += "\\/" + } 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 +} From e6bdaa2f1ce73585bff5cf0f0063649170e2277c Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Wed, 24 May 2023 17:57:45 +0300 Subject: [PATCH 03/13] Fix --- models/repo/repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/repo/repo.go b/models/repo/repo.go index 42945bc624d6b..d3e6daa95b53a 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -382,7 +382,7 @@ func (repo *Repository) LoadOwner(ctx context.Context) (err error) { return err } -// MustOwner always returns a valid *user_model.Users object to avoid +// MustOwner always returns a valid *user_model.User object to avoid // conceptually impossible error handling. // It creates a fake object that contains error details // when error occurs. From b7ec465b84882352360f062ed43666de93da3be0 Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov <81759784+cl-bvl@users.noreply.github.com> Date: Wed, 24 May 2023 18:04:24 +0300 Subject: [PATCH 04/13] Update modules/git/repo_show.go Co-authored-by: techknowlogick --- modules/git/repo_show.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/git/repo_show.go b/modules/git/repo_show.go index c1ae4fd99e305..9b9d325466446 100644 --- a/modules/git/repo_show.go +++ b/modules/git/repo_show.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. +// Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package git From 7136afe7ca15dd675174a57f7c32a09ffae5d937 Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Thu, 25 May 2023 10:50:02 +0300 Subject: [PATCH 05/13] Fixes --- services/pull/pull.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/pull/pull.go b/services/pull/pull.go index 884c738139e1f..d70e2d5513254 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -125,7 +125,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issu _, _ = issue_service.CreateComment(ctx, ops) - if coRules, err := GetCodeOwners(ctx, repo, pr.BaseBranch); err == nil { + if coRules, err := GetCodeOwners(ctx, repo, pr.BaseBranch); err == nil && len(coRules) > 0 { changedFiles, err := baseGitRepo.GetFilesChangedBetween(git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) if err != nil { return err @@ -1003,8 +1003,6 @@ func tokenizeCodeOwnersLine(line string) []string { escape = true } else if string(char) == "#" { break - //} else if string(char) == "/" { - // token += "\\/" } else if string(char) == " " { if len(token) > 0 { tokens = append(tokens, token) From c7d71a8f75db4c41748bc3e7333e3280f1857a09 Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Thu, 25 May 2023 12:50:14 +0300 Subject: [PATCH 06/13] Fix doc --- docs/content/doc/usage/code-owners.en-us.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/content/doc/usage/code-owners.en-us.md b/docs/content/doc/usage/code-owners.en-us.md index 0cdb5ba82e329..a8f56cfee7088 100644 --- a/docs/content/doc/usage/code-owners.en-us.md +++ b/docs/content/doc/usage/code-owners.en-us.md @@ -47,14 +47,14 @@ docs/(aws|google|azure)/[^/]*\\.(md|txt) @user8 @org1/team4 You can escape characters `#`, ` ` (space) and `\` with `\`, like: ``` -/dir/with\#hashtag @user1 -/path\ with\ space @user2 -/path/with\\backslash @user3 +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 +path/\\.with\\.dots +path/with\\+plus ``` From d3d4732bfa37a2417bdd560207730d644d37cd27 Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Sun, 28 May 2023 17:41:43 +0300 Subject: [PATCH 07/13] Add file verify --- docs/content/doc/usage/code-owners.en-us.md | 7 ++- modules/git/repo_show.go | 20 ------- routers/web/repo/view.go | 9 +++ services/pull/check.go | 17 +++--- services/pull/pull.go | 62 +++++++++++++-------- 5 files changed, 64 insertions(+), 51 deletions(-) delete mode 100644 modules/git/repo_show.go diff --git a/docs/content/doc/usage/code-owners.en-us.md b/docs/content/doc/usage/code-owners.en-us.md index a8f56cfee7088..94f81eeae1a38 100644 --- a/docs/content/doc/usage/code-owners.en-us.md +++ b/docs/content/doc/usage/code-owners.en-us.md @@ -25,6 +25,11 @@ Gitea maintains code owner files. It looks for it in the following locations in And stops at the first found file. +File format: ` <@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: ``` @@ -32,7 +37,7 @@ Example file: # Comment too # You can assigning code owning for users or teams -frontend/src/.*\\.js @org1/team1 @org1/team2 +frontend/src/.*\\.js @org1/team1 @org1/team2 @user3 # You can use negative pattern !frontend/src/.* @org1/team3 @user5 diff --git a/modules/git/repo_show.go b/modules/git/repo_show.go deleted file mode 100644 index 9b9d325466446..0000000000000 --- a/modules/git/repo_show.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "fmt" -) - -// GetFileContent returns file content for given revision. -func (repo *Repository) GetFileContent(rev, path string) ([]byte, error) { - cmd := NewCommand(repo.Ctx, "show") - cmd.AddDynamicArguments(fmt.Sprintf("%s:%s", rev, path)) - stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) - if err != nil { - return nil, err - } - - return stdout, nil -} diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 2fd893f91c6dd..47f08b9789fe9 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -41,6 +41,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" issue_service "code.gitea.io/gitea/services/issue" + "code.gitea.io/gitea/services/pull" "github.com/nektos/act/pkg/model" ) @@ -361,6 +362,14 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st if workFlowErr != nil { ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) } + } else if util.SliceContains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { + if data, err := blob.GetBlobContent(); err == nil { + _, warnings := pull.GetCodeOwnersFromContent(ctx, data) + if len(warnings) > 0 { + ctx.Data["FileWarning"] = strings.Join(warnings, "\n") + } + } + } isDisplayingSource := ctx.FormString("display") == "source" diff --git a/services/pull/check.go b/services/pull/check.go index 8bc2bdff1d176..bcb591971713f 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -357,14 +357,15 @@ func testPR(id int64) { return } - if err := TestPatch(pr); err != nil { - log.Error("testPatch[%-v]: %v", pr, err) - pr.Status = issues_model.PullRequestStatusError - if err := pr.UpdateCols("status"); err != nil { - log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err) - } - return - } + //if err := TestPatch(pr); err != nil { + // log.Error("testPatch[%-v]: %v", pr, err) + // pr.Status = issues_model.PullRequestStatusError + // if err := pr.UpdateCols("status"); err != nil { + // log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err) + // } + // return + //} + TestPatch(pr) checkAndUpdateStatus(ctx, pr) } diff --git a/services/pull/pull.go b/services/pull/pull.go index d70e2d5513254..f15d97cb863ef 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -125,14 +125,13 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issu _, _ = issue_service.CreateComment(ctx, ops) - if coRules, err := GetCodeOwners(ctx, repo, pr.BaseBranch); err == nil && len(coRules) > 0 { + 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) { @@ -872,42 +871,55 @@ func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, br // 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, error) { +func GetCodeOwners(ctx context.Context, repo *repo_model.Repository, branch string) ([]*CodeOwnerRule, []string, error) { files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath()) if err != nil { - return nil, err + return nil, nil, err } defer closer.Close() if !gitRepo.IsBranchExist(branch) { - return nil, &git.ErrBranchNotExist{Name: branch} + return nil, nil, &git.ErrBranchNotExist{Name: branch} } - var data []byte + commit, err := gitRepo.GetBranchCommit(branch) + + var data string for _, file := range files { - data, err = gitRepo.GetFileContent(branch, file) - if err == nil { - break + if blob, err := commit.GetBlobByPath(file); err == nil { + data, err = blob.GetBlobContent() + if err == nil { + break + } } } + rules, warnings := GetCodeOwnersFromContent(ctx, data) + 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(string(data), "\n") + lines := strings.Split(data, "\n") + warnings := make([]string, 0) - for _, line := range lines { + for i, line := range lines { tokens := tokenizeCodeOwnersLine(line) if len(tokens) == 0 { continue } else if len(tokens) < 2 { - log.Info("Incorrect codeowner line: %s", line) + warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1)) continue } - rule := parseCodeOwnersLine(ctx, tokens) + rule, wr := parseCodeOwnersLine(ctx, tokens) + for _, w := range wr { + warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w)) + } if rule == nil { continue } @@ -915,7 +927,7 @@ func GetCodeOwners(ctx context.Context, repo *repo_model.Repository, branch stri rules = append(rules, rule) } - return rules, nil + return rules, warnings } type CodeOwnerRule struct { @@ -924,17 +936,19 @@ type CodeOwnerRule struct { Users []*user_model.User } -func parseCodeOwnersLine(ctx context.Context, tokens []string) *CodeOwnerRule { +func parseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, []string) { 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 { - log.Info("Incorrect codeowner regexp: %s, error: %s", tokens[0], err) - return nil + warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err)) + return nil, warnings } for _, user := range tokens[1:] { @@ -944,7 +958,7 @@ func parseCodeOwnersLine(ctx context.Context, tokens []string) *CodeOwnerRule { if strings.Contains(user, "/") { s := strings.Split(user, "/") if len(s) != 2 { - log.Info("Incorrect codeowner group: %s", user) + warnings = append(warnings, fmt.Sprintf("incorrect codeowner group: %s", user)) continue } orgName := s[0] @@ -952,11 +966,13 @@ func parseCodeOwnersLine(ctx context.Context, tokens []string) *CodeOwnerRule { org, err := org_model.GetOrgByName(ctx, orgName) if err != nil { - log.Info("Incorrect codeowner org name: %s", user) + warnings = append(warnings, fmt.Sprintf("incorrect codeowner organization: %s", user)) + continue } teams, err := org.LoadTeams() if err != nil { - log.Info("Incorrect codeowner team name: %s", user) + warnings = append(warnings, fmt.Sprintf("incorrect codeowner team: %s", user)) + continue } for _, team := range teams { @@ -970,6 +986,7 @@ func parseCodeOwnersLine(ctx context.Context, tokens []string) *CodeOwnerRule { } 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) @@ -977,10 +994,11 @@ func parseCodeOwnersLine(ctx context.Context, tokens []string) *CodeOwnerRule { } if len(rule.Users) == 0 { - return nil + warnings = append(warnings, "no users matched") + return nil, warnings } - return rule + return rule, warnings } func tokenizeCodeOwnersLine(line string) []string { From dba89f13593bce5c9e21be7d5e6bbf15b9bcb828 Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Mon, 29 May 2023 12:20:43 +0300 Subject: [PATCH 08/13] Revert --- services/pull/check.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/services/pull/check.go b/services/pull/check.go index bcb591971713f..8bc2bdff1d176 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -357,15 +357,14 @@ func testPR(id int64) { return } - //if err := TestPatch(pr); err != nil { - // log.Error("testPatch[%-v]: %v", pr, err) - // pr.Status = issues_model.PullRequestStatusError - // if err := pr.UpdateCols("status"); err != nil { - // log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err) - // } - // return - //} - TestPatch(pr) + if err := TestPatch(pr); err != nil { + log.Error("testPatch[%-v]: %v", pr, err) + pr.Status = issues_model.PullRequestStatusError + if err := pr.UpdateCols("status"); err != nil { + log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err) + } + return + } checkAndUpdateStatus(ctx, pr) } From b679754d757d5642aedc0fa3bd46973ee0b2bfb8 Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Wed, 31 May 2023 20:30:55 +0300 Subject: [PATCH 09/13] Add WIP support --- models/issues/pull.go | 221 +++++++++++++++++++++++++++++++++++++++ routers/web/repo/view.go | 8 +- services/issue/issue.go | 12 +++ services/pull/pull.go | 198 +---------------------------------- 4 files changed, 240 insertions(+), 199 deletions(-) diff --git a/models/issues/pull.go b/models/issues/pull.go index 218a265741e2b..983692feb474b 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -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" @@ -863,3 +865,222 @@ func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *gi func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 } + +func NotifyCodeOwners(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 { + 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 +} diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 47f08b9789fe9..5dca3eeaa92d4 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -16,11 +16,14 @@ import ( "strings" "time" + "github.com/nektos/act/pkg/model" + activities_model "code.gitea.io/gitea/models/activities" admin_model "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" + issue_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -41,9 +44,6 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" issue_service "code.gitea.io/gitea/services/issue" - "code.gitea.io/gitea/services/pull" - - "github.com/nektos/act/pkg/model" ) const ( @@ -364,7 +364,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st } } else if util.SliceContains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { if data, err := blob.GetBlobContent(); err == nil { - _, warnings := pull.GetCodeOwnersFromContent(ctx, data) + _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) if len(warnings) > 0 { ctx.Data["FileWarning"] = strings.Join(warnings, "\n") } diff --git a/services/issue/issue.go b/services/issue/issue.go index d4f827e99af56..a2f1dee24416a 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -57,6 +57,18 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode return } + if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { + if err = issue.LoadPullRequest(ctx); err != nil { + return + } + + if issue.PullRequest == nil { + return fmt.Errorf("PR is nill") + } + + issues_model.NotifyCodeOwners(ctx, issue, issue.PullRequest) + } + notification.NotifyIssueChangeTitle(ctx, doer, issue, oldTitle) return nil diff --git a/services/pull/pull.go b/services/pull/pull.go index f15d97cb863ef..09f0a10081660 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -15,7 +15,6 @@ 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" @@ -125,31 +124,12 @@ 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 { + if !pr.IsWorkInProgress() { + if err := issues_model.NotifyCodeOwners(ctx, pull, pr); 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 @@ -865,175 +845,3 @@ 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"} - 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) - 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) { - 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 -} From a801f70785d72b84255846bf25cc30c6d524f67d Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Wed, 31 May 2023 20:35:38 +0300 Subject: [PATCH 10/13] Fixes --- public/serviceworker.js | 1 + services/issue/issue.go | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) create mode 100644 public/serviceworker.js diff --git a/public/serviceworker.js b/public/serviceworker.js new file mode 100644 index 0000000000000..c7d0731760aab --- /dev/null +++ b/public/serviceworker.js @@ -0,0 +1 @@ +var c=(T,x,f)=>new Promise((S,h)=>{var b=d=>{try{R(f.next(d))}catch(u){h(u)}},m=d=>{try{R(f.throw(d))}catch(u){h(u)}},R=d=>d.done?S(d.value):Promise.resolve(d.value).then(b,m);R((f=f.apply(T,x)).next())});(function(){"use strict";var T={"./node_modules/workbox-core/_version.js":function(){try{self["workbox:core:6.5.3"]&&_()}catch(h){}},"./node_modules/workbox-routing/_version.js":function(){try{self["workbox:routing:6.5.3"]&&_()}catch(h){}},"./node_modules/workbox-strategies/_version.js":function(){try{self["workbox:strategies:6.5.3"]&&_()}catch(h){}}},x={};function f(h){var b=x[h];if(b!==void 0)return b.exports;var m=x[h]={exports:{}};return T[h](m,m.exports,f),m.exports}var S={};(function(){var h=f("./node_modules/workbox-core/_version.js");const b={"invalid-value":({paramName:r,validValueDescription:e,value:t})=>{if(!r||!e)throw new Error("Unexpected input to 'invalid-value' error.");return`The '${r}' parameter was given a value with an unexpected value. ${e} Received a value of ${JSON.stringify(t)}.`},"not-an-array":({moduleName:r,className:e,funcName:t,paramName:s})=>{if(!r||!e||!t||!s)throw new Error("Unexpected input to 'not-an-array' error.");return`The parameter '${s}' passed into '${r}.${e}.${t}()' must be an array.`},"incorrect-type":({expectedType:r,paramName:e,moduleName:t,className:s,funcName:n})=>{if(!r||!e||!t||!n)throw new Error("Unexpected input to 'incorrect-type' error.");const o=s?`${s}.`:"";return`The parameter '${e}' passed into '${t}.${o}${n}()' must be of type ${r}.`},"incorrect-class":({expectedClassName:r,paramName:e,moduleName:t,className:s,funcName:n,isReturnValueProblem:o})=>{if(!r||!t||!n)throw new Error("Unexpected input to 'incorrect-class' error.");const a=s?`${s}.`:"";return o?`The return value from '${t}.${a}${n}()' must be an instance of class ${r}.`:`The parameter '${e}' passed into '${t}.${a}${n}()' must be an instance of class ${r}.`},"missing-a-method":({expectedMethod:r,paramName:e,moduleName:t,className:s,funcName:n})=>{if(!r||!e||!t||!s||!n)throw new Error("Unexpected input to 'missing-a-method' error.");return`${t}.${s}.${n}() expected the '${e}' parameter to expose a '${r}' method.`},"add-to-cache-list-unexpected-type":({entry:r})=>`An unexpected entry was passed to 'workbox-precaching.PrecacheController.addToCacheList()' The entry '${JSON.stringify(r)}' isn't supported. You must supply an array of strings with one or more characters, objects with a url property or Request objects.`,"add-to-cache-list-conflicting-entries":({firstEntry:r,secondEntry:e})=>{if(!r||!e)throw new Error("Unexpected input to 'add-to-cache-list-duplicate-entries' error.");return`Two of the entries passed to 'workbox-precaching.PrecacheController.addToCacheList()' had the URL ${r} but different revision details. Workbox is unable to cache and version the asset correctly. Please remove one of the entries.`},"plugin-error-request-will-fetch":({thrownErrorMessage:r})=>{if(!r)throw new Error("Unexpected input to 'plugin-error-request-will-fetch', error.");return`An error was thrown by a plugins 'requestWillFetch()' method. The thrown error message was: '${r}'.`},"invalid-cache-name":({cacheNameId:r,value:e})=>{if(!r)throw new Error("Expected a 'cacheNameId' for error 'invalid-cache-name'");return`You must provide a name containing at least one character for setCacheDetails({${r}: '...'}). Received a value of '${JSON.stringify(e)}'`},"unregister-route-but-not-found-with-method":({method:r})=>{if(!r)throw new Error("Unexpected input to 'unregister-route-but-not-found-with-method' error.");return`The route you're trying to unregister was not previously registered for the method type '${r}'.`},"unregister-route-route-not-registered":()=>"The route you're trying to unregister was not previously registered.","queue-replay-failed":({name:r})=>`Replaying the background sync queue '${r}' failed.`,"duplicate-queue-name":({name:r})=>`The Queue name '${r}' is already being used. All instances of backgroundSync.Queue must be given unique names.`,"expired-test-without-max-age":({methodName:r,paramName:e})=>`The '${r}()' method can only be used when the '${e}' is used in the constructor.`,"unsupported-route-type":({moduleName:r,className:e,funcName:t,paramName:s})=>`The supplied '${s}' parameter was an unsupported type. Please check the docs for ${r}.${e}.${t} for valid input types.`,"not-array-of-class":({value:r,expectedClass:e,moduleName:t,className:s,funcName:n,paramName:o})=>`The supplied '${o}' parameter must be an array of '${e}' objects. Received '${JSON.stringify(r)},'. Please check the call to ${t}.${s}.${n}() to fix the issue.`,"max-entries-or-age-required":({moduleName:r,className:e,funcName:t})=>`You must define either config.maxEntries or config.maxAgeSecondsin ${r}.${e}.${t}`,"statuses-or-headers-required":({moduleName:r,className:e,funcName:t})=>`You must define either config.statuses or config.headersin ${r}.${e}.${t}`,"invalid-string":({moduleName:r,funcName:e,paramName:t})=>{if(!t||!r||!e)throw new Error("Unexpected input to 'invalid-string' error.");return`When using strings, the '${t}' parameter must start with 'http' (for cross-origin matches) or '/' (for same-origin matches). Please see the docs for ${r}.${e}() for more info.`},"channel-name-required":()=>"You must provide a channelName to construct a BroadcastCacheUpdate instance.","invalid-responses-are-same-args":()=>"The arguments passed into responsesAreSame() appear to be invalid. Please ensure valid Responses are used.","expire-custom-caches-only":()=>"You must provide a 'cacheName' property when using the expiration plugin with a runtime caching strategy.","unit-must-be-bytes":({normalizedRangeHeader:r})=>{if(!r)throw new Error("Unexpected input to 'unit-must-be-bytes' error.");return`The 'unit' portion of the Range header must be set to 'bytes'. The Range header provided was "${r}"`},"single-range-only":({normalizedRangeHeader:r})=>{if(!r)throw new Error("Unexpected input to 'single-range-only' error.");return`Multiple ranges are not supported. Please use a single start value, and optional end value. The Range header provided was "${r}"`},"invalid-range-values":({normalizedRangeHeader:r})=>{if(!r)throw new Error("Unexpected input to 'invalid-range-values' error.");return`The Range header is missing both start and end values. At least one of those values is needed. The Range header provided was "${r}"`},"no-range-header":()=>"No Range header was found in the Request provided.","range-not-satisfiable":({size:r,start:e,end:t})=>`The start (${e}) and end (${t}) values in the Range are not satisfiable by the cached response, which is ${r} bytes.`,"attempt-to-cache-non-get-request":({url:r,method:e})=>`Unable to cache '${r}' because it is a '${e}' request and only 'GET' requests can be cached.`,"cache-put-with-no-response":({url:r})=>`There was an attempt to cache '${r}' but the response was not defined.`,"no-response":({url:r,error:e})=>{let t=`The strategy could not generate a response for '${r}'.`;return e&&(t+=` The underlying error is ${e}.`),t},"bad-precaching-response":({url:r,status:e})=>`The precaching request for '${r}' failed`+(e?` with an HTTP status of ${e}.`:"."),"non-precached-url":({url:r})=>`createHandlerBoundToURL('${r}') was called, but that URL is not precached. Please pass in a URL that is precached instead.`,"add-to-cache-list-conflicting-integrities":({url:r})=>`Two of the entries passed to 'workbox-precaching.PrecacheController.addToCacheList()' had the URL ${r} with different integrity values. Please remove one of them.`,"missing-precache-entry":({cacheName:r,url:e})=>`Unable to find a precached response in ${r} for ${e}.`,"cross-origin-copy-response":({origin:r})=>`workbox-core.copyResponse() can only be used with same-origin responses. It was passed a response with origin ${r}.`,"opaque-streams-source":({type:r})=>{const e=`One of the workbox-streams sources resulted in an '${r}' response.`;return r==="opaqueredirect"?`${e} Please do not use a navigation request that results in a redirect as a source.`:`${e} Please ensure your sources are CORS-enabled.`}},m=(r,...e)=>{let t=r;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},R=(r,e={})=>{const t=messages[r];if(!t)throw new Error(`Unable to find message for code '${r}'.`);return t(e)},d=m;class u extends Error{constructor(e,t){const s=d(e,t);super(s),this.name=e,this.details=t}}const z=(r,e)=>{if(!Array.isArray(r))throw new WorkboxError("not-an-array",e)},ee=(r,e,t)=>{if(typeof r[e]!=="function")throw t.expectedMethod=e,new WorkboxError("missing-a-method",t)},te=(r,e,t)=>{if(typeof r!==e)throw t.expectedType=e,new WorkboxError("incorrect-type",t)},re=(r,e,t)=>{if(!(r instanceof e))throw t.expectedClassName=e.name,new WorkboxError("incorrect-class",t)},se=(r,e,t)=>{if(!e.includes(r))throw t.validValueDescription=`Valid values are ${JSON.stringify(e)}.`,new WorkboxError("invalid-value",t)},ne=(r,e,t)=>{const s=new WorkboxError("not-array-of-class",t);if(!Array.isArray(r))throw s;for(const n of r)if(!(n instanceof e))throw s},ae=null,P=null;var oe=f("./node_modules/workbox-routing/_version.js");const O="GET",ie=null,$=r=>r&&typeof r=="object"?r:{handle:r};class v{constructor(e,t,s=O){this.handler=$(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=$(e)}}class ce extends null{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}class W extends v{constructor(e,t,s){const n=({url:o})=>{const a=e.exec(o.href);if(a&&!(o.origin!==location.origin&&a.index!==0))return a.slice(1)};super(n,t,s)}}const q=r=>new URL(String(r),location.href).href.replace(new RegExp(`^${location.origin}`),"");class L{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)})}addCacheListener(){self.addEventListener("message",e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const o=new Request(...n);return this.handleRequest({request:o,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}})}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:o,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=a&&a.handler;const l=[],y=e.method;if(!i&&this._defaultHandlerMap.has(y)&&(i=this._defaultHandlerMap.get(y)),!i)return;let w;try{w=i.handle({url:s,request:e,event:t,params:o})}catch(E){w=Promise.reject(E)}const g=a&&a.catchHandler;return w instanceof Promise&&(this._catchHandler||g)&&(w=w.catch(E=>c(this,null,function*(){if(g)try{return yield g.handle({url:s,request:e,event:t,params:o})}catch(D){D instanceof Error&&(E=D)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw E}))),w}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const o=this._routes.get(s.method)||[];for(const a of o){let i;const l=a.match({url:e,sameOrigin:t,request:s,event:n});if(l)return i=l,(Array.isArray(i)&&i.length===0||l.constructor===Object&&Object.keys(l).length===0||typeof l=="boolean")&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=O){this._defaultHandlerMap.set(t,$(e))}setCatchHandler(e){this._catchHandler=$(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new u("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new u("unregister-route-route-not-registered")}}let k;const M=()=>(k||(k=new L,k.addFetchListener(),k.addCacheListener()),k);function j(r,e,t){let s;if(typeof r=="string"){const o=new URL(r,location.href),a=({url:i})=>i.href===o.href;s=new v(a,e,t)}else if(r instanceof RegExp)s=new W(r,e,t);else if(typeof r=="function")s=new v(r,e,t);else if(r instanceof v)s=r;else throw new u("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return M().registerRoute(s),s}function le(r){getOrCreateDefaultRouter().setCatchHandler(r)}function ue(r){getOrCreateDefaultRouter().setDefaultHandler(r)}const p={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration!="undefined"?registration.scope:""},U=r=>[p.prefix,r,p.suffix].filter(e=>e&&e.length>0).join("-"),H=r=>{for(const e of Object.keys(p))r(e)},F={updateDetails:r=>{H(e=>{typeof r[e]=="string"&&(p[e]=r[e])})},getGoogleAnalyticsName:r=>r||U(p.googleAnalytics),getPrecacheName:r=>r||U(p.precache),getPrefix:()=>p.prefix,getRuntimeName:r=>r||U(p.runtime),getSuffix:()=>p.suffix};function A(r,e){const t=new URL(r);for(const s of e)t.searchParams.delete(s);return t.href}function N(r,e,t,s){return c(this,null,function*(){const n=A(e.url,t);if(e.url===n)return r.match(e,s);const o=Object.assign(Object.assign({},s),{ignoreSearch:!0}),a=yield r.keys(e,o);for(const i of a){const l=A(i.url,t);if(n===l)return r.match(i,s)}})}class K{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}const B=new Set;function G(){return c(this,null,function*(){for(const r of B)yield r()})}function I(r){return new Promise(e=>setTimeout(e,r))}var he=f("./node_modules/workbox-strategies/_version.js");function C(r){return typeof r=="string"?new Request(r):r}class J{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new K,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const s of this._plugins)this._pluginStateMap.set(s,{});this.event.waitUntil(this._handlerDeferred.promise)}fetch(e){return c(this,null,function*(){const{event:t}=this;let s=C(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const a=yield t.preloadResponse;if(a)return a}const n=this.hasCallback("fetchDidFail")?s.clone():null;try{for(const a of this.iterateCallbacks("requestWillFetch"))s=yield a({request:s.clone(),event:t})}catch(a){if(a instanceof Error)throw new u("plugin-error-request-will-fetch",{thrownErrorMessage:a.message})}const o=s.clone();try{let a;a=yield fetch(s,s.mode==="navigate"?void 0:this._strategy.fetchOptions);for(const i of this.iterateCallbacks("fetchDidSucceed"))a=yield i({event:t,request:o,response:a});return a}catch(a){throw n&&(yield this.runCallbacks("fetchDidFail",{error:a,event:t,originalRequest:n.clone(),request:o.clone()})),a}})}fetchAndCachePut(e){return c(this,null,function*(){const t=yield this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t})}cacheMatch(e){return c(this,null,function*(){const t=C(e);let s;const{cacheName:n,matchOptions:o}=this._strategy,a=yield this.getCacheKey(t,"read"),i=Object.assign(Object.assign({},o),{cacheName:n});s=yield caches.match(a,i);for(const l of this.iterateCallbacks("cachedResponseWillBeUsed"))s=(yield l({cacheName:n,matchOptions:o,cachedResponse:s,request:a,event:this.event}))||void 0;return s})}cachePut(e,t){return c(this,null,function*(){const s=C(e);yield I(0);const n=yield this.getCacheKey(s,"write");if(!t)throw new u("cache-put-with-no-response",{url:q(n.url)});const o=yield this._ensureResponseSafeToCache(t);if(!o)return!1;const{cacheName:a,matchOptions:i}=this._strategy,l=yield self.caches.open(a),y=this.hasCallback("cacheDidUpdate"),w=y?yield N(l,n.clone(),["__WB_REVISION__"],i):null;try{yield l.put(n,y?o.clone():o)}catch(g){if(g instanceof Error)throw g.name==="QuotaExceededError"&&(yield G()),g}for(const g of this.iterateCallbacks("cacheDidUpdate"))yield g({cacheName:a,oldResponse:w,newResponse:o.clone(),request:n,event:this.event});return!0})}getCacheKey(e,t){return c(this,null,function*(){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let n=e;for(const o of this.iterateCallbacks("cacheKeyWillBeUsed"))n=C(yield o({mode:t,request:n,event:this.event,params:this.params}));this._cacheKeys[s]=n}return this._cacheKeys[s]})}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}runCallbacks(e,t){return c(this,null,function*(){for(const s of this.iterateCallbacks(e))yield s(t)})}*iterateCallbacks(e){for(const t of this._strategy.plugins)if(typeof t[e]=="function"){const s=this._pluginStateMap.get(t);yield o=>{const a=Object.assign(Object.assign({},o),{state:s});return t[e](a)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}doneWaiting(){return c(this,null,function*(){let e;for(;e=this._extendLifetimePromises.shift();)yield e})}destroy(){this._handlerDeferred.resolve(null)}_ensureResponseSafeToCache(e){return c(this,null,function*(){let t=e,s=!1;for(const n of this.iterateCallbacks("cacheWillUpdate"))if(t=(yield n({request:this.request,response:t,event:this.event}))||void 0,s=!0,!t)break;return s||t&&t.status!==200&&(t=void 0),t})}}class Y{constructor(e={}){this.cacheName=F.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s=typeof e.request=="string"?new Request(e.request):e.request,n="params"in e?e.params:void 0,o=new J(this,{event:t,request:s,params:n}),a=this._getResponse(o,s,t),i=this._awaitComplete(a,o,s,t);return[a,i]}_getResponse(e,t,s){return c(this,null,function*(){yield e.runCallbacks("handlerWillStart",{event:s,request:t});let n;try{if(n=yield this._handle(t,e),!n||n.type==="error")throw new u("no-response",{url:t.url})}catch(o){if(o instanceof Error){for(const a of e.iterateCallbacks("handlerDidError"))if(n=yield a({error:o,event:s,request:t}),n)break}if(!n)throw o}for(const o of e.iterateCallbacks("handlerWillRespond"))n=yield o({event:s,request:t,response:n});return n})}_awaitComplete(e,t,s,n){return c(this,null,function*(){let o,a;try{o=yield e}catch(i){}try{yield t.runCallbacks("handlerDidRespond",{event:n,request:s,response:o}),yield t.doneWaiting()}catch(i){i instanceof Error&&(a=i)}if(yield t.runCallbacks("handlerDidComplete",{event:n,request:s,response:o,error:a}),t.destroy(),a)throw a})}}const fe={strategyStart:(r,e)=>`Using ${r} to respond to '${q(e.url)}'`,printFinalResponse:r=>{r&&(P.groupCollapsed("View the final response here."),P.log(r||"[No response returned]"),P.groupEnd())}};class de extends null{_handle(e,t){return c(this,null,function*(){const s=[];let n=yield t.cacheMatch(e),o;if(!n)try{n=yield t.fetchAndCachePut(e)}catch(a){a instanceof Error&&(o=a)}if(!n)throw new WorkboxError("no-response",{url:e.url,error:o});return n})}}class pe extends null{_handle(e,t){return c(this,null,function*(){const s=yield t.cacheMatch(e);if(!s)throw new WorkboxError("no-response",{url:e.url});return s})}}const Q={cacheWillUpdate:e=>c(this,[e],function*({response:r}){return r.status===200||r.status===0?r:null})};class ge extends null{constructor(e={}){super(e),this.plugins.some(t=>"cacheWillUpdate"in t)||this.plugins.unshift(cacheOkAndOpaquePlugin),this._networkTimeoutSeconds=e.networkTimeoutSeconds||0}_handle(e,t){return c(this,null,function*(){const s=[],n=[];let o;if(this._networkTimeoutSeconds){const{id:l,promise:y}=this._getTimeoutPromise({request:e,logs:s,handler:t});o=l,n.push(y)}const a=this._getNetworkPromise({timeoutId:o,request:e,logs:s,handler:t});n.push(a);const i=yield t.waitUntil((()=>c(this,null,function*(){return(yield t.waitUntil(Promise.race(n)))||(yield a)}))());if(!i)throw new WorkboxError("no-response",{url:e.url});return i})}_getTimeoutPromise({request:e,logs:t,handler:s}){let n;return{promise:new Promise(a=>{n=setTimeout(()=>c(this,null,function*(){a(yield s.cacheMatch(e))}),this._networkTimeoutSeconds*1e3)}),id:n}}_getNetworkPromise(o){return c(this,arguments,function*({timeoutId:e,request:t,logs:s,handler:n}){let a,i;try{i=yield n.fetchAndCachePut(t)}catch(l){l instanceof Error&&(a=l)}return e&&clearTimeout(e),(a||!i)&&(i=yield n.cacheMatch(t)),i})}}class we extends null{constructor(e={}){super(e),this._networkTimeoutSeconds=e.networkTimeoutSeconds||0}_handle(e,t){return c(this,null,function*(){let s,n;try{const o=[t.fetch(e)];if(this._networkTimeoutSeconds){const a=timeout(this._networkTimeoutSeconds*1e3);o.push(a)}if(n=yield Promise.race(o),!n)throw new Error(`Timed out the network response after ${this._networkTimeoutSeconds} seconds.`)}catch(o){o instanceof Error&&(s=o)}if(!n)throw new WorkboxError("no-response",{url:e.url,error:s});return n})}}class V extends Y{constructor(e={}){super(e),this.plugins.some(t=>"cacheWillUpdate"in t)||this.plugins.unshift(Q)}_handle(e,t){return c(this,null,function*(){const s=[],n=t.fetchAndCachePut(e).catch(()=>{});t.waitUntil(n);let o=yield t.cacheMatch(e),a;if(!o)try{o=yield n}catch(i){i instanceof Error&&(a=i)}if(!o)throw new u("no-response",{url:e.url,error:a});return o})}}const X="static-cache-v2";self.__WB_DISABLE_DEV_LOGS=!0;const Z=new Set(["font","manifest","paintworklet","script","sharedworker","style","worker"]);j(({request:r})=>Z.has(r.destination),new V({cacheName:X}))})()})(); diff --git a/services/issue/issue.go b/services/issue/issue.go index 80ea41caffaf1..01e44ada19ec2 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -58,14 +58,6 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode } if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { - if err = issue.LoadPullRequest(ctx); err != nil { - return - } - - if issue.PullRequest == nil { - return fmt.Errorf("PR is nill") - } - issues_model.NotifyCodeOwners(ctx, issue, issue.PullRequest) } From ad0a3a592794be2974c0e701bb263e6aba9ac61e Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Wed, 31 May 2023 20:38:25 +0300 Subject: [PATCH 11/13] Fixes --- models/issues/pull.go | 2 +- public/serviceworker.js | 1 - services/issue/issue.go | 2 +- services/pull/pull.go | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 public/serviceworker.js diff --git a/models/issues/pull.go b/models/issues/pull.go index 513217550dbcb..223d07b9a449d 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -890,7 +890,7 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr * return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 } -func NotifyCodeOwners(ctx context.Context, pull *Issue, pr *PullRequest) error { +func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullRequest) error { files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} if pr.IsWorkInProgress() { diff --git a/public/serviceworker.js b/public/serviceworker.js deleted file mode 100644 index c7d0731760aab..0000000000000 --- a/public/serviceworker.js +++ /dev/null @@ -1 +0,0 @@ -var c=(T,x,f)=>new Promise((S,h)=>{var b=d=>{try{R(f.next(d))}catch(u){h(u)}},m=d=>{try{R(f.throw(d))}catch(u){h(u)}},R=d=>d.done?S(d.value):Promise.resolve(d.value).then(b,m);R((f=f.apply(T,x)).next())});(function(){"use strict";var T={"./node_modules/workbox-core/_version.js":function(){try{self["workbox:core:6.5.3"]&&_()}catch(h){}},"./node_modules/workbox-routing/_version.js":function(){try{self["workbox:routing:6.5.3"]&&_()}catch(h){}},"./node_modules/workbox-strategies/_version.js":function(){try{self["workbox:strategies:6.5.3"]&&_()}catch(h){}}},x={};function f(h){var b=x[h];if(b!==void 0)return b.exports;var m=x[h]={exports:{}};return T[h](m,m.exports,f),m.exports}var S={};(function(){var h=f("./node_modules/workbox-core/_version.js");const b={"invalid-value":({paramName:r,validValueDescription:e,value:t})=>{if(!r||!e)throw new Error("Unexpected input to 'invalid-value' error.");return`The '${r}' parameter was given a value with an unexpected value. ${e} Received a value of ${JSON.stringify(t)}.`},"not-an-array":({moduleName:r,className:e,funcName:t,paramName:s})=>{if(!r||!e||!t||!s)throw new Error("Unexpected input to 'not-an-array' error.");return`The parameter '${s}' passed into '${r}.${e}.${t}()' must be an array.`},"incorrect-type":({expectedType:r,paramName:e,moduleName:t,className:s,funcName:n})=>{if(!r||!e||!t||!n)throw new Error("Unexpected input to 'incorrect-type' error.");const o=s?`${s}.`:"";return`The parameter '${e}' passed into '${t}.${o}${n}()' must be of type ${r}.`},"incorrect-class":({expectedClassName:r,paramName:e,moduleName:t,className:s,funcName:n,isReturnValueProblem:o})=>{if(!r||!t||!n)throw new Error("Unexpected input to 'incorrect-class' error.");const a=s?`${s}.`:"";return o?`The return value from '${t}.${a}${n}()' must be an instance of class ${r}.`:`The parameter '${e}' passed into '${t}.${a}${n}()' must be an instance of class ${r}.`},"missing-a-method":({expectedMethod:r,paramName:e,moduleName:t,className:s,funcName:n})=>{if(!r||!e||!t||!s||!n)throw new Error("Unexpected input to 'missing-a-method' error.");return`${t}.${s}.${n}() expected the '${e}' parameter to expose a '${r}' method.`},"add-to-cache-list-unexpected-type":({entry:r})=>`An unexpected entry was passed to 'workbox-precaching.PrecacheController.addToCacheList()' The entry '${JSON.stringify(r)}' isn't supported. You must supply an array of strings with one or more characters, objects with a url property or Request objects.`,"add-to-cache-list-conflicting-entries":({firstEntry:r,secondEntry:e})=>{if(!r||!e)throw new Error("Unexpected input to 'add-to-cache-list-duplicate-entries' error.");return`Two of the entries passed to 'workbox-precaching.PrecacheController.addToCacheList()' had the URL ${r} but different revision details. Workbox is unable to cache and version the asset correctly. Please remove one of the entries.`},"plugin-error-request-will-fetch":({thrownErrorMessage:r})=>{if(!r)throw new Error("Unexpected input to 'plugin-error-request-will-fetch', error.");return`An error was thrown by a plugins 'requestWillFetch()' method. The thrown error message was: '${r}'.`},"invalid-cache-name":({cacheNameId:r,value:e})=>{if(!r)throw new Error("Expected a 'cacheNameId' for error 'invalid-cache-name'");return`You must provide a name containing at least one character for setCacheDetails({${r}: '...'}). Received a value of '${JSON.stringify(e)}'`},"unregister-route-but-not-found-with-method":({method:r})=>{if(!r)throw new Error("Unexpected input to 'unregister-route-but-not-found-with-method' error.");return`The route you're trying to unregister was not previously registered for the method type '${r}'.`},"unregister-route-route-not-registered":()=>"The route you're trying to unregister was not previously registered.","queue-replay-failed":({name:r})=>`Replaying the background sync queue '${r}' failed.`,"duplicate-queue-name":({name:r})=>`The Queue name '${r}' is already being used. All instances of backgroundSync.Queue must be given unique names.`,"expired-test-without-max-age":({methodName:r,paramName:e})=>`The '${r}()' method can only be used when the '${e}' is used in the constructor.`,"unsupported-route-type":({moduleName:r,className:e,funcName:t,paramName:s})=>`The supplied '${s}' parameter was an unsupported type. Please check the docs for ${r}.${e}.${t} for valid input types.`,"not-array-of-class":({value:r,expectedClass:e,moduleName:t,className:s,funcName:n,paramName:o})=>`The supplied '${o}' parameter must be an array of '${e}' objects. Received '${JSON.stringify(r)},'. Please check the call to ${t}.${s}.${n}() to fix the issue.`,"max-entries-or-age-required":({moduleName:r,className:e,funcName:t})=>`You must define either config.maxEntries or config.maxAgeSecondsin ${r}.${e}.${t}`,"statuses-or-headers-required":({moduleName:r,className:e,funcName:t})=>`You must define either config.statuses or config.headersin ${r}.${e}.${t}`,"invalid-string":({moduleName:r,funcName:e,paramName:t})=>{if(!t||!r||!e)throw new Error("Unexpected input to 'invalid-string' error.");return`When using strings, the '${t}' parameter must start with 'http' (for cross-origin matches) or '/' (for same-origin matches). Please see the docs for ${r}.${e}() for more info.`},"channel-name-required":()=>"You must provide a channelName to construct a BroadcastCacheUpdate instance.","invalid-responses-are-same-args":()=>"The arguments passed into responsesAreSame() appear to be invalid. Please ensure valid Responses are used.","expire-custom-caches-only":()=>"You must provide a 'cacheName' property when using the expiration plugin with a runtime caching strategy.","unit-must-be-bytes":({normalizedRangeHeader:r})=>{if(!r)throw new Error("Unexpected input to 'unit-must-be-bytes' error.");return`The 'unit' portion of the Range header must be set to 'bytes'. The Range header provided was "${r}"`},"single-range-only":({normalizedRangeHeader:r})=>{if(!r)throw new Error("Unexpected input to 'single-range-only' error.");return`Multiple ranges are not supported. Please use a single start value, and optional end value. The Range header provided was "${r}"`},"invalid-range-values":({normalizedRangeHeader:r})=>{if(!r)throw new Error("Unexpected input to 'invalid-range-values' error.");return`The Range header is missing both start and end values. At least one of those values is needed. The Range header provided was "${r}"`},"no-range-header":()=>"No Range header was found in the Request provided.","range-not-satisfiable":({size:r,start:e,end:t})=>`The start (${e}) and end (${t}) values in the Range are not satisfiable by the cached response, which is ${r} bytes.`,"attempt-to-cache-non-get-request":({url:r,method:e})=>`Unable to cache '${r}' because it is a '${e}' request and only 'GET' requests can be cached.`,"cache-put-with-no-response":({url:r})=>`There was an attempt to cache '${r}' but the response was not defined.`,"no-response":({url:r,error:e})=>{let t=`The strategy could not generate a response for '${r}'.`;return e&&(t+=` The underlying error is ${e}.`),t},"bad-precaching-response":({url:r,status:e})=>`The precaching request for '${r}' failed`+(e?` with an HTTP status of ${e}.`:"."),"non-precached-url":({url:r})=>`createHandlerBoundToURL('${r}') was called, but that URL is not precached. Please pass in a URL that is precached instead.`,"add-to-cache-list-conflicting-integrities":({url:r})=>`Two of the entries passed to 'workbox-precaching.PrecacheController.addToCacheList()' had the URL ${r} with different integrity values. Please remove one of them.`,"missing-precache-entry":({cacheName:r,url:e})=>`Unable to find a precached response in ${r} for ${e}.`,"cross-origin-copy-response":({origin:r})=>`workbox-core.copyResponse() can only be used with same-origin responses. It was passed a response with origin ${r}.`,"opaque-streams-source":({type:r})=>{const e=`One of the workbox-streams sources resulted in an '${r}' response.`;return r==="opaqueredirect"?`${e} Please do not use a navigation request that results in a redirect as a source.`:`${e} Please ensure your sources are CORS-enabled.`}},m=(r,...e)=>{let t=r;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},R=(r,e={})=>{const t=messages[r];if(!t)throw new Error(`Unable to find message for code '${r}'.`);return t(e)},d=m;class u extends Error{constructor(e,t){const s=d(e,t);super(s),this.name=e,this.details=t}}const z=(r,e)=>{if(!Array.isArray(r))throw new WorkboxError("not-an-array",e)},ee=(r,e,t)=>{if(typeof r[e]!=="function")throw t.expectedMethod=e,new WorkboxError("missing-a-method",t)},te=(r,e,t)=>{if(typeof r!==e)throw t.expectedType=e,new WorkboxError("incorrect-type",t)},re=(r,e,t)=>{if(!(r instanceof e))throw t.expectedClassName=e.name,new WorkboxError("incorrect-class",t)},se=(r,e,t)=>{if(!e.includes(r))throw t.validValueDescription=`Valid values are ${JSON.stringify(e)}.`,new WorkboxError("invalid-value",t)},ne=(r,e,t)=>{const s=new WorkboxError("not-array-of-class",t);if(!Array.isArray(r))throw s;for(const n of r)if(!(n instanceof e))throw s},ae=null,P=null;var oe=f("./node_modules/workbox-routing/_version.js");const O="GET",ie=null,$=r=>r&&typeof r=="object"?r:{handle:r};class v{constructor(e,t,s=O){this.handler=$(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=$(e)}}class ce extends null{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}class W extends v{constructor(e,t,s){const n=({url:o})=>{const a=e.exec(o.href);if(a&&!(o.origin!==location.origin&&a.index!==0))return a.slice(1)};super(n,t,s)}}const q=r=>new URL(String(r),location.href).href.replace(new RegExp(`^${location.origin}`),"");class L{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)})}addCacheListener(){self.addEventListener("message",e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const o=new Request(...n);return this.handleRequest({request:o,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}})}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:o,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=a&&a.handler;const l=[],y=e.method;if(!i&&this._defaultHandlerMap.has(y)&&(i=this._defaultHandlerMap.get(y)),!i)return;let w;try{w=i.handle({url:s,request:e,event:t,params:o})}catch(E){w=Promise.reject(E)}const g=a&&a.catchHandler;return w instanceof Promise&&(this._catchHandler||g)&&(w=w.catch(E=>c(this,null,function*(){if(g)try{return yield g.handle({url:s,request:e,event:t,params:o})}catch(D){D instanceof Error&&(E=D)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw E}))),w}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const o=this._routes.get(s.method)||[];for(const a of o){let i;const l=a.match({url:e,sameOrigin:t,request:s,event:n});if(l)return i=l,(Array.isArray(i)&&i.length===0||l.constructor===Object&&Object.keys(l).length===0||typeof l=="boolean")&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=O){this._defaultHandlerMap.set(t,$(e))}setCatchHandler(e){this._catchHandler=$(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new u("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new u("unregister-route-route-not-registered")}}let k;const M=()=>(k||(k=new L,k.addFetchListener(),k.addCacheListener()),k);function j(r,e,t){let s;if(typeof r=="string"){const o=new URL(r,location.href),a=({url:i})=>i.href===o.href;s=new v(a,e,t)}else if(r instanceof RegExp)s=new W(r,e,t);else if(typeof r=="function")s=new v(r,e,t);else if(r instanceof v)s=r;else throw new u("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return M().registerRoute(s),s}function le(r){getOrCreateDefaultRouter().setCatchHandler(r)}function ue(r){getOrCreateDefaultRouter().setDefaultHandler(r)}const p={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration!="undefined"?registration.scope:""},U=r=>[p.prefix,r,p.suffix].filter(e=>e&&e.length>0).join("-"),H=r=>{for(const e of Object.keys(p))r(e)},F={updateDetails:r=>{H(e=>{typeof r[e]=="string"&&(p[e]=r[e])})},getGoogleAnalyticsName:r=>r||U(p.googleAnalytics),getPrecacheName:r=>r||U(p.precache),getPrefix:()=>p.prefix,getRuntimeName:r=>r||U(p.runtime),getSuffix:()=>p.suffix};function A(r,e){const t=new URL(r);for(const s of e)t.searchParams.delete(s);return t.href}function N(r,e,t,s){return c(this,null,function*(){const n=A(e.url,t);if(e.url===n)return r.match(e,s);const o=Object.assign(Object.assign({},s),{ignoreSearch:!0}),a=yield r.keys(e,o);for(const i of a){const l=A(i.url,t);if(n===l)return r.match(i,s)}})}class K{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}const B=new Set;function G(){return c(this,null,function*(){for(const r of B)yield r()})}function I(r){return new Promise(e=>setTimeout(e,r))}var he=f("./node_modules/workbox-strategies/_version.js");function C(r){return typeof r=="string"?new Request(r):r}class J{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new K,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const s of this._plugins)this._pluginStateMap.set(s,{});this.event.waitUntil(this._handlerDeferred.promise)}fetch(e){return c(this,null,function*(){const{event:t}=this;let s=C(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const a=yield t.preloadResponse;if(a)return a}const n=this.hasCallback("fetchDidFail")?s.clone():null;try{for(const a of this.iterateCallbacks("requestWillFetch"))s=yield a({request:s.clone(),event:t})}catch(a){if(a instanceof Error)throw new u("plugin-error-request-will-fetch",{thrownErrorMessage:a.message})}const o=s.clone();try{let a;a=yield fetch(s,s.mode==="navigate"?void 0:this._strategy.fetchOptions);for(const i of this.iterateCallbacks("fetchDidSucceed"))a=yield i({event:t,request:o,response:a});return a}catch(a){throw n&&(yield this.runCallbacks("fetchDidFail",{error:a,event:t,originalRequest:n.clone(),request:o.clone()})),a}})}fetchAndCachePut(e){return c(this,null,function*(){const t=yield this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t})}cacheMatch(e){return c(this,null,function*(){const t=C(e);let s;const{cacheName:n,matchOptions:o}=this._strategy,a=yield this.getCacheKey(t,"read"),i=Object.assign(Object.assign({},o),{cacheName:n});s=yield caches.match(a,i);for(const l of this.iterateCallbacks("cachedResponseWillBeUsed"))s=(yield l({cacheName:n,matchOptions:o,cachedResponse:s,request:a,event:this.event}))||void 0;return s})}cachePut(e,t){return c(this,null,function*(){const s=C(e);yield I(0);const n=yield this.getCacheKey(s,"write");if(!t)throw new u("cache-put-with-no-response",{url:q(n.url)});const o=yield this._ensureResponseSafeToCache(t);if(!o)return!1;const{cacheName:a,matchOptions:i}=this._strategy,l=yield self.caches.open(a),y=this.hasCallback("cacheDidUpdate"),w=y?yield N(l,n.clone(),["__WB_REVISION__"],i):null;try{yield l.put(n,y?o.clone():o)}catch(g){if(g instanceof Error)throw g.name==="QuotaExceededError"&&(yield G()),g}for(const g of this.iterateCallbacks("cacheDidUpdate"))yield g({cacheName:a,oldResponse:w,newResponse:o.clone(),request:n,event:this.event});return!0})}getCacheKey(e,t){return c(this,null,function*(){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let n=e;for(const o of this.iterateCallbacks("cacheKeyWillBeUsed"))n=C(yield o({mode:t,request:n,event:this.event,params:this.params}));this._cacheKeys[s]=n}return this._cacheKeys[s]})}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}runCallbacks(e,t){return c(this,null,function*(){for(const s of this.iterateCallbacks(e))yield s(t)})}*iterateCallbacks(e){for(const t of this._strategy.plugins)if(typeof t[e]=="function"){const s=this._pluginStateMap.get(t);yield o=>{const a=Object.assign(Object.assign({},o),{state:s});return t[e](a)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}doneWaiting(){return c(this,null,function*(){let e;for(;e=this._extendLifetimePromises.shift();)yield e})}destroy(){this._handlerDeferred.resolve(null)}_ensureResponseSafeToCache(e){return c(this,null,function*(){let t=e,s=!1;for(const n of this.iterateCallbacks("cacheWillUpdate"))if(t=(yield n({request:this.request,response:t,event:this.event}))||void 0,s=!0,!t)break;return s||t&&t.status!==200&&(t=void 0),t})}}class Y{constructor(e={}){this.cacheName=F.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s=typeof e.request=="string"?new Request(e.request):e.request,n="params"in e?e.params:void 0,o=new J(this,{event:t,request:s,params:n}),a=this._getResponse(o,s,t),i=this._awaitComplete(a,o,s,t);return[a,i]}_getResponse(e,t,s){return c(this,null,function*(){yield e.runCallbacks("handlerWillStart",{event:s,request:t});let n;try{if(n=yield this._handle(t,e),!n||n.type==="error")throw new u("no-response",{url:t.url})}catch(o){if(o instanceof Error){for(const a of e.iterateCallbacks("handlerDidError"))if(n=yield a({error:o,event:s,request:t}),n)break}if(!n)throw o}for(const o of e.iterateCallbacks("handlerWillRespond"))n=yield o({event:s,request:t,response:n});return n})}_awaitComplete(e,t,s,n){return c(this,null,function*(){let o,a;try{o=yield e}catch(i){}try{yield t.runCallbacks("handlerDidRespond",{event:n,request:s,response:o}),yield t.doneWaiting()}catch(i){i instanceof Error&&(a=i)}if(yield t.runCallbacks("handlerDidComplete",{event:n,request:s,response:o,error:a}),t.destroy(),a)throw a})}}const fe={strategyStart:(r,e)=>`Using ${r} to respond to '${q(e.url)}'`,printFinalResponse:r=>{r&&(P.groupCollapsed("View the final response here."),P.log(r||"[No response returned]"),P.groupEnd())}};class de extends null{_handle(e,t){return c(this,null,function*(){const s=[];let n=yield t.cacheMatch(e),o;if(!n)try{n=yield t.fetchAndCachePut(e)}catch(a){a instanceof Error&&(o=a)}if(!n)throw new WorkboxError("no-response",{url:e.url,error:o});return n})}}class pe extends null{_handle(e,t){return c(this,null,function*(){const s=yield t.cacheMatch(e);if(!s)throw new WorkboxError("no-response",{url:e.url});return s})}}const Q={cacheWillUpdate:e=>c(this,[e],function*({response:r}){return r.status===200||r.status===0?r:null})};class ge extends null{constructor(e={}){super(e),this.plugins.some(t=>"cacheWillUpdate"in t)||this.plugins.unshift(cacheOkAndOpaquePlugin),this._networkTimeoutSeconds=e.networkTimeoutSeconds||0}_handle(e,t){return c(this,null,function*(){const s=[],n=[];let o;if(this._networkTimeoutSeconds){const{id:l,promise:y}=this._getTimeoutPromise({request:e,logs:s,handler:t});o=l,n.push(y)}const a=this._getNetworkPromise({timeoutId:o,request:e,logs:s,handler:t});n.push(a);const i=yield t.waitUntil((()=>c(this,null,function*(){return(yield t.waitUntil(Promise.race(n)))||(yield a)}))());if(!i)throw new WorkboxError("no-response",{url:e.url});return i})}_getTimeoutPromise({request:e,logs:t,handler:s}){let n;return{promise:new Promise(a=>{n=setTimeout(()=>c(this,null,function*(){a(yield s.cacheMatch(e))}),this._networkTimeoutSeconds*1e3)}),id:n}}_getNetworkPromise(o){return c(this,arguments,function*({timeoutId:e,request:t,logs:s,handler:n}){let a,i;try{i=yield n.fetchAndCachePut(t)}catch(l){l instanceof Error&&(a=l)}return e&&clearTimeout(e),(a||!i)&&(i=yield n.cacheMatch(t)),i})}}class we extends null{constructor(e={}){super(e),this._networkTimeoutSeconds=e.networkTimeoutSeconds||0}_handle(e,t){return c(this,null,function*(){let s,n;try{const o=[t.fetch(e)];if(this._networkTimeoutSeconds){const a=timeout(this._networkTimeoutSeconds*1e3);o.push(a)}if(n=yield Promise.race(o),!n)throw new Error(`Timed out the network response after ${this._networkTimeoutSeconds} seconds.`)}catch(o){o instanceof Error&&(s=o)}if(!n)throw new WorkboxError("no-response",{url:e.url,error:s});return n})}}class V extends Y{constructor(e={}){super(e),this.plugins.some(t=>"cacheWillUpdate"in t)||this.plugins.unshift(Q)}_handle(e,t){return c(this,null,function*(){const s=[],n=t.fetchAndCachePut(e).catch(()=>{});t.waitUntil(n);let o=yield t.cacheMatch(e),a;if(!o)try{o=yield n}catch(i){i instanceof Error&&(a=i)}if(!o)throw new u("no-response",{url:e.url,error:a});return o})}}const X="static-cache-v2";self.__WB_DISABLE_DEV_LOGS=!0;const Z=new Set(["font","manifest","paintworklet","script","sharedworker","style","worker"]);j(({request:r})=>Z.has(r.destination),new V({cacheName:X}))})()})(); diff --git a/services/issue/issue.go b/services/issue/issue.go index 01e44ada19ec2..cb99fc04d89cf 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -58,7 +58,7 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode } if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { - issues_model.NotifyCodeOwners(ctx, issue, issue.PullRequest) + issues_model.PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest) } notification.NotifyIssueChangeTitle(ctx, doer, issue, oldTitle) diff --git a/services/pull/pull.go b/services/pull/pull.go index 09f0a10081660..f44e690ab7087 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -125,7 +125,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, pull *issu _, _ = issue_service.CreateComment(ctx, ops) if !pr.IsWorkInProgress() { - if err := issues_model.NotifyCodeOwners(ctx, pull, pr); err != nil { + if err := issues_model.PullRequestCodeOwnersReview(ctx, pull, pr); err != nil { return err } } From 76b86ef77a9d325c4d64b7f4fc0284f459ac13d1 Mon Sep 17 00:00:00 2001 From: Vladimir Buyanov Date: Thu, 1 Jun 2023 23:15:19 +0300 Subject: [PATCH 12/13] Add tests --- models/issues/pull.go | 12 ++++++------ models/issues/pull_test.go | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/models/issues/pull.go b/models/issues/pull.go index 223d07b9a449d..63bc258d2f39f 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -981,14 +981,14 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul warnings := make([]string, 0) for i, line := range lines { - tokens := tokenizeCodeOwnersLine(line) + 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) + rule, wr := ParseCodeOwnersLine(ctx, tokens) for _, w := range wr { warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w)) } @@ -1009,7 +1009,7 @@ type CodeOwnerRule struct { Teams []*org_model.Team } -func parseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, []string) { +func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, []string) { var err error rule := &CodeOwnerRule{ Users: make([]*user_model.User, 0), @@ -1064,15 +1064,15 @@ func parseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, } } - if len(rule.Users) == 0 { - warnings = append(warnings, "no users matched") + 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 { +func TokenizeCodeOwnersLine(line string) []string { if len(line) == 0 { return nil } diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 7a183ac3121c2..dd13df99b14fd 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -303,3 +303,26 @@ func TestDeleteOrphanedObjects(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, countBefore, countAfter) } + +func TestParseCodeOwnersLine(t *testing.T) { + type CodeOwnerTest struct { + Line string + Tokens []string + } + + given := []CodeOwnerTest{ + {Line: "", Tokens: nil}, + {Line: "# comment", Tokens: []string{}}, + {Line: "!.* @user1 @org1/team1", Tokens: []string{"!.*", "@user1", "@org1/team1"}}, + {Line: `.*\\.js @user2 #comment`, Tokens: []string{`.*\.js`, "@user2"}}, + {Line: `docs/(aws|google|azure)/[^/]*\\.(md|txt) @user3 @org2/team2`, Tokens: []string{`docs/(aws|google|azure)/[^/]*\.(md|txt)`, "@user3", "@org2/team2"}}, + {Line: `\#path @user3`, Tokens: []string{`#path`, "@user3"}}, + {Line: `path\ with\ spaces/ @user3`, Tokens: []string{`path with spaces/`, "@user3"}}, + } + + for _, g := range given { + tokens := issues_model.TokenizeCodeOwnersLine(g.Line) + assert.Equal(t, g.Tokens, tokens, "Codeowners tokenizer failed") + } + +} From 7ddd65474e6bb252e87af21e08bb735d0eef49bc Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 4 Jun 2023 14:20:01 +0800 Subject: [PATCH 13/13] Fix lint --- models/issues/pull_test.go | 1 - routers/web/repo/view.go | 5 ++--- services/issue/issue.go | 4 +++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index dd13df99b14fd..1eb106047cea4 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -324,5 +324,4 @@ func TestParseCodeOwnersLine(t *testing.T) { tokens := issues_model.TokenizeCodeOwnersLine(g.Line) assert.Equal(t, g.Tokens, tokens, "Codeowners tokenizer failed") } - } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 5dca3eeaa92d4..1d54f25884e39 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -16,8 +16,6 @@ import ( "strings" "time" - "github.com/nektos/act/pkg/model" - activities_model "code.gitea.io/gitea/models/activities" admin_model "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -44,6 +42,8 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" issue_service "code.gitea.io/gitea/services/issue" + + "github.com/nektos/act/pkg/model" ) const ( @@ -369,7 +369,6 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st ctx.Data["FileWarning"] = strings.Join(warnings, "\n") } } - } isDisplayingSource := ctx.FormString("display") == "source" diff --git a/services/issue/issue.go b/services/issue/issue.go index cb99fc04d89cf..61890c75def09 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -58,7 +58,9 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode } if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { - issues_model.PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest) + if err = issues_model.PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest); err != nil { + return + } } notification.NotifyIssueChangeTitle(ctx, doer, issue, oldTitle)