loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request '[Port] gitea#29842: Notify reviewers added via CODEOWNERS' (#2855) from gusted/forgejo-port-29842 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2855
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

+308 -102
-87
models/issues/pull.go
··· 19 19 repo_model "code.gitea.io/gitea/models/repo" 20 20 user_model "code.gitea.io/gitea/models/user" 21 21 "code.gitea.io/gitea/modules/git" 22 - "code.gitea.io/gitea/modules/gitrepo" 23 22 "code.gitea.io/gitea/modules/log" 24 23 "code.gitea.io/gitea/modules/setting" 25 24 "code.gitea.io/gitea/modules/timeutil" ··· 882 881 // MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch 883 882 func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool { 884 883 return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0 885 - } 886 - 887 - func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullRequest) error { 888 - files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} 889 - 890 - if pr.IsWorkInProgress(ctx) { 891 - return nil 892 - } 893 - 894 - if err := pull.LoadRepo(ctx); err != nil { 895 - return err 896 - } 897 - 898 - if pull.Repo.IsFork { 899 - return nil 900 - } 901 - 902 - if err := pr.LoadBaseRepo(ctx); err != nil { 903 - return err 904 - } 905 - 906 - repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) 907 - if err != nil { 908 - return err 909 - } 910 - defer repo.Close() 911 - 912 - commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch) 913 - if err != nil { 914 - return err 915 - } 916 - 917 - var data string 918 - for _, file := range files { 919 - if blob, err := commit.GetBlobByPath(file); err == nil { 920 - data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize) 921 - if err == nil { 922 - break 923 - } 924 - } 925 - } 926 - 927 - rules, _ := GetCodeOwnersFromContent(ctx, data) 928 - 929 - prInfo, err := repo.GetCompareInfo(repo.Path, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName(), false, false) 930 - if err != nil { 931 - return err 932 - } 933 - // Use the merge base as the base instead of the main branch to avoid problems 934 - // if the pull request is out of date with the base branch. 935 - changedFiles, err := repo.GetFilesChangedBetween(prInfo.MergeBase, prInfo.HeadCommitID) 936 - if err != nil { 937 - return err 938 - } 939 - 940 - uniqUsers := make(map[int64]*user_model.User) 941 - uniqTeams := make(map[string]*org_model.Team) 942 - for _, rule := range rules { 943 - for _, f := range changedFiles { 944 - if (rule.Rule.MatchString(f) && !rule.Negative) || (!rule.Rule.MatchString(f) && rule.Negative) { 945 - for _, u := range rule.Users { 946 - uniqUsers[u.ID] = u 947 - } 948 - for _, t := range rule.Teams { 949 - uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t 950 - } 951 - } 952 - } 953 - } 954 - 955 - for _, u := range uniqUsers { 956 - if u.ID != pull.Poster.ID { 957 - if _, err := AddReviewRequest(ctx, pull, u, pull.Poster); err != nil { 958 - log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err) 959 - return err 960 - } 961 - } 962 - } 963 - for _, t := range uniqTeams { 964 - if _, err := AddTeamReviewRequest(ctx, pull, t, pull.Poster); err != nil { 965 - log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err) 966 - return err 967 - } 968 - } 969 - 970 - return nil 971 884 } 972 885 973 886 // GetCodeOwnersFromContent returns the code owners configuration
+20 -3
services/issue/assignee.go
··· 226 226 return nil, nil 227 227 } 228 228 229 + return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment) 230 + } 231 + 232 + func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifers []*ReviewRequestNotifier) { 233 + for _, reviewNotifer := range reviewNotifers { 234 + if reviewNotifer.Reviwer != nil { 235 + notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifer.Reviwer, reviewNotifer.IsAdd, reviewNotifer.Comment) 236 + } else if reviewNotifer.ReviewTeam != nil { 237 + if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifer.ReviewTeam, reviewNotifer.IsAdd, reviewNotifer.Comment); err != nil { 238 + log.Error("teamReviewRequestNotify: %v", err) 239 + } 240 + } 241 + } 242 + } 243 + 244 + // teamReviewRequestNotify notify all user in this team 245 + func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool, comment *issues_model.Comment) error { 229 246 // notify all user in this team 230 247 if err := comment.LoadIssue(ctx); err != nil { 231 - return nil, err 248 + return err 232 249 } 233 250 234 251 members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{ 235 252 TeamID: reviewer.ID, 236 253 }) 237 254 if err != nil { 238 - return nil, err 255 + return err 239 256 } 240 257 241 258 for _, member := range members { ··· 246 263 notify_service.PullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment) 247 264 } 248 265 249 - return comment, err 266 + return err 250 267 } 251 268 252 269 // CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR
+15 -5
services/issue/issue.go
··· 64 64 return nil 65 65 } 66 66 67 - if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil { 68 - return err 69 - } 67 + var reviewNotifers []*ReviewRequestNotifier 70 68 71 - if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { 72 - if err := issues_model.PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest); err != nil { 69 + if err := db.WithTx(ctx, func(ctx context.Context) error { 70 + if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil { 73 71 return err 74 72 } 73 + 74 + if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { 75 + var err error 76 + reviewNotifers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest) 77 + if err != nil { 78 + return err 79 + } 80 + } 81 + return nil 82 + }); err != nil { 83 + return err 75 84 } 76 85 77 86 notify_service.IssueChangeTitle(ctx, doer, issue, oldTitle) 87 + ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers) 78 88 79 89 return nil 80 90 }
+143
services/issue/pull.go
··· 1 + // Copyright 2024 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package issue 5 + 6 + import ( 7 + "context" 8 + "fmt" 9 + "time" 10 + 11 + issues_model "code.gitea.io/gitea/models/issues" 12 + org_model "code.gitea.io/gitea/models/organization" 13 + user_model "code.gitea.io/gitea/models/user" 14 + "code.gitea.io/gitea/modules/git" 15 + "code.gitea.io/gitea/modules/gitrepo" 16 + "code.gitea.io/gitea/modules/log" 17 + "code.gitea.io/gitea/modules/setting" 18 + ) 19 + 20 + func getMergeBase(repo *git.Repository, pr *issues_model.PullRequest, baseBranch, headBranch string) (string, error) { 21 + // Add a temporary remote 22 + tmpRemote := fmt.Sprintf("mergebase-%d-%d", pr.ID, time.Now().UnixNano()) 23 + if err := repo.AddRemote(tmpRemote, repo.Path, false); err != nil { 24 + return "", fmt.Errorf("AddRemote: %w", err) 25 + } 26 + defer func() { 27 + if err := repo.RemoveRemote(tmpRemote); err != nil { 28 + log.Error("getMergeBase: RemoveRemote: %v", err) 29 + } 30 + }() 31 + 32 + mergeBase, _, err := repo.GetMergeBase(tmpRemote, baseBranch, headBranch) 33 + return mergeBase, err 34 + } 35 + 36 + type ReviewRequestNotifier struct { 37 + Comment *issues_model.Comment 38 + IsAdd bool 39 + Reviwer *user_model.User 40 + ReviewTeam *org_model.Team 41 + } 42 + 43 + func PullRequestCodeOwnersReview(ctx context.Context, pull *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) { 44 + files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"} 45 + 46 + if pr.IsWorkInProgress(ctx) { 47 + return nil, nil 48 + } 49 + 50 + if err := pr.LoadHeadRepo(ctx); err != nil { 51 + return nil, err 52 + } 53 + 54 + if pr.HeadRepo.IsFork { 55 + return nil, nil 56 + } 57 + 58 + if err := pr.LoadBaseRepo(ctx); err != nil { 59 + return nil, err 60 + } 61 + 62 + repo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) 63 + if err != nil { 64 + return nil, err 65 + } 66 + defer repo.Close() 67 + 68 + commit, err := repo.GetBranchCommit(pr.BaseRepo.DefaultBranch) 69 + if err != nil { 70 + return nil, err 71 + } 72 + 73 + var data string 74 + for _, file := range files { 75 + if blob, err := commit.GetBlobByPath(file); err == nil { 76 + data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize) 77 + if err == nil { 78 + break 79 + } 80 + } 81 + } 82 + 83 + rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data) 84 + 85 + // get the mergebase 86 + mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName()) 87 + if err != nil { 88 + return nil, err 89 + } 90 + 91 + // https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed 92 + // between the merge base and the head commit but not the base branch and the head commit 93 + changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.HeadCommitID) 94 + if err != nil { 95 + return nil, err 96 + } 97 + 98 + uniqUsers := make(map[int64]*user_model.User) 99 + uniqTeams := make(map[string]*org_model.Team) 100 + for _, rule := range rules { 101 + for _, f := range changedFiles { 102 + if (rule.Rule.MatchString(f) && !rule.Negative) || (!rule.Rule.MatchString(f) && rule.Negative) { 103 + for _, u := range rule.Users { 104 + uniqUsers[u.ID] = u 105 + } 106 + for _, t := range rule.Teams { 107 + uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t 108 + } 109 + } 110 + } 111 + } 112 + 113 + notifiers := make([]*ReviewRequestNotifier, 0, len(uniqUsers)+len(uniqTeams)) 114 + 115 + for _, u := range uniqUsers { 116 + if u.ID != pull.Poster.ID { 117 + comment, err := issues_model.AddReviewRequest(ctx, pull, u, pull.Poster) 118 + if err != nil { 119 + log.Warn("Failed add assignee user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err) 120 + return nil, err 121 + } 122 + notifiers = append(notifiers, &ReviewRequestNotifier{ 123 + Comment: comment, 124 + IsAdd: true, 125 + Reviwer: pull.Poster, 126 + }) 127 + } 128 + } 129 + for _, t := range uniqTeams { 130 + comment, err := issues_model.AddTeamReviewRequest(ctx, pull, t, pull.Poster) 131 + if err != nil { 132 + log.Warn("Failed add assignee team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err) 133 + return nil, err 134 + } 135 + notifiers = append(notifiers, &ReviewRequestNotifier{ 136 + Comment: comment, 137 + IsAdd: true, 138 + ReviewTeam: t, 139 + }) 140 + } 141 + 142 + return notifiers, nil 143 + }
+5 -2
services/pull/pull.go
··· 74 74 } 75 75 defer baseGitRepo.Close() 76 76 77 + var reviewNotifers []*issue_service.ReviewRequestNotifier 77 78 if err := db.WithTx(ctx, func(ctx context.Context) error { 78 79 if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil { 79 80 return err ··· 133 134 } 134 135 135 136 if !pr.IsWorkInProgress(ctx) { 136 - if err := issues_model.PullRequestCodeOwnersReview(ctx, issue, pr); err != nil { 137 + reviewNotifers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr) 138 + if err != nil { 137 139 return err 138 140 } 139 141 } ··· 147 149 } 148 150 baseGitRepo.Close() // close immediately to avoid notifications will open the repository again 149 151 152 + issue_service.ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers) 153 + 150 154 mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content) 151 155 if err != nil { 152 156 return err 153 157 } 154 - 155 158 notify_service.NewPullRequest(ctx, pr, mentions) 156 159 if len(issue.Labels) > 0 { 157 160 notify_service.IssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil)
+125 -5
tests/integration/pull_review_test.go
··· 6 6 import ( 7 7 "context" 8 8 "net/http" 9 + "net/url" 9 10 "strconv" 11 + "strings" 10 12 "testing" 11 13 12 - "code.gitea.io/gitea/models/issues" 14 + "code.gitea.io/gitea/models/db" 15 + issues_model "code.gitea.io/gitea/models/issues" 13 16 "code.gitea.io/gitea/models/unittest" 17 + user_model "code.gitea.io/gitea/models/user" 18 + "code.gitea.io/gitea/modules/git" 19 + repo_service "code.gitea.io/gitea/services/repository" 20 + files_service "code.gitea.io/gitea/services/repository/files" 14 21 "code.gitea.io/gitea/tests" 15 22 16 23 "github.com/PuerkitoBio/goquery" ··· 28 35 session.MakeRequest(t, req, http.StatusOK) 29 36 } 30 37 31 - func loadComment(t *testing.T, commentID string) *issues.Comment { 38 + func loadComment(t *testing.T, commentID string) *issues_model.Comment { 32 39 t.Helper() 33 40 id, err := strconv.ParseInt(commentID, 10, 64) 34 41 assert.NoError(t, err) 35 - return unittest.AssertExistsAndLoadBean(t, &issues.Comment{ID: id}) 42 + return unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: id}) 36 43 } 37 44 38 45 func TestPullView_ResolveInvalidatedReviewComment(t *testing.T) { ··· 81 88 // (to invalidate it properly, one should push a commit which should trigger this logic, 82 89 // in the meantime, use this quick-and-dirty trick) 83 90 comment := loadComment(t, commentID) 84 - assert.NoError(t, issues.UpdateCommentInvalidate(context.Background(), &issues.Comment{ 91 + assert.NoError(t, issues_model.UpdateCommentInvalidate(context.Background(), &issues_model.Comment{ 85 92 ID: comment.ID, 86 93 Invalidated: true, 87 94 })) ··· 143 150 // (to invalidate it properly, one should push a commit which should trigger this logic, 144 151 // in the meantime, use this quick-and-dirty trick) 145 152 comment := loadComment(t, commentID) 146 - assert.NoError(t, issues.UpdateCommentInvalidate(context.Background(), &issues.Comment{ 153 + assert.NoError(t, issues_model.UpdateCommentInvalidate(context.Background(), &issues_model.Comment{ 147 154 ID: comment.ID, 148 155 Invalidated: true, 149 156 })) ··· 250 257 assert.Len(t, comments.Nodes, 3) // 1 comment on line 1 + 2 comments on line 3 251 258 }) 252 259 } 260 + 261 + func TestPullView_CodeOwner(t *testing.T) { 262 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 263 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 264 + 265 + // Create the repo. 266 + repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{ 267 + Name: "test_codeowner", 268 + Readme: "Default", 269 + AutoInit: true, 270 + ObjectFormatName: git.Sha1ObjectFormat.Name(), 271 + DefaultBranch: "master", 272 + }) 273 + assert.NoError(t, err) 274 + 275 + // add CODEOWNERS to default branch 276 + _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ 277 + OldBranch: repo.DefaultBranch, 278 + Files: []*files_service.ChangeRepoFile{ 279 + { 280 + Operation: "create", 281 + TreePath: "CODEOWNERS", 282 + ContentReader: strings.NewReader("README.md @user5\n"), 283 + }, 284 + }, 285 + }) 286 + assert.NoError(t, err) 287 + 288 + t.Run("First Pull Request", func(t *testing.T) { 289 + // create a new branch to prepare for pull request 290 + _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ 291 + NewBranch: "codeowner-basebranch", 292 + Files: []*files_service.ChangeRepoFile{ 293 + { 294 + Operation: "update", 295 + TreePath: "README.md", 296 + ContentReader: strings.NewReader("# This is a new project\n"), 297 + }, 298 + }, 299 + }) 300 + assert.NoError(t, err) 301 + 302 + // Create a pull request. 303 + session := loginUser(t, "user2") 304 + testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch", "Test Pull Request") 305 + 306 + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch"}) 307 + unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5}) 308 + }) 309 + 310 + // change the default branch CODEOWNERS file to change README.md's codeowner 311 + _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ 312 + Files: []*files_service.ChangeRepoFile{ 313 + { 314 + Operation: "update", 315 + TreePath: "CODEOWNERS", 316 + ContentReader: strings.NewReader("README.md @user8\n"), 317 + }, 318 + }, 319 + }) 320 + assert.NoError(t, err) 321 + 322 + t.Run("Second Pull Request", func(t *testing.T) { 323 + // create a new branch to prepare for pull request 324 + _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{ 325 + NewBranch: "codeowner-basebranch2", 326 + Files: []*files_service.ChangeRepoFile{ 327 + { 328 + Operation: "update", 329 + TreePath: "README.md", 330 + ContentReader: strings.NewReader("# This is a new project2\n"), 331 + }, 332 + }, 333 + }) 334 + assert.NoError(t, err) 335 + 336 + // Create a pull request. 337 + session := loginUser(t, "user2") 338 + testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch2", "Test Pull Request2") 339 + 340 + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"}) 341 + unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) 342 + }) 343 + 344 + t.Run("Forked Repo Pull Request", func(t *testing.T) { 345 + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) 346 + forkedRepo, err := repo_service.ForkRepository(db.DefaultContext, user2, user5, repo_service.ForkRepoOptions{ 347 + BaseRepo: repo, 348 + Name: "test_codeowner_fork", 349 + }) 350 + assert.NoError(t, err) 351 + 352 + // create a new branch to prepare for pull request 353 + _, err = files_service.ChangeRepoFiles(db.DefaultContext, forkedRepo, user5, &files_service.ChangeRepoFilesOptions{ 354 + NewBranch: "codeowner-basebranch-forked", 355 + Files: []*files_service.ChangeRepoFile{ 356 + { 357 + Operation: "update", 358 + TreePath: "README.md", 359 + ContentReader: strings.NewReader("# This is a new forked project\n"), 360 + }, 361 + }, 362 + }) 363 + assert.NoError(t, err) 364 + 365 + session := loginUser(t, "user5") 366 + testPullCreate(t, session, "user5", "test_codeowner_fork", false, forkedRepo.DefaultBranch, "codeowner-basebranch-forked", "Test Pull Request2") 367 + 368 + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch-forked"}) 369 + unittest.AssertExistsIf(t, false, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8}) 370 + }) 371 + }) 372 + }