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.

feat: add commit limit for webhook payload (#6797)

- Adds a new option `[webhook].PAYLOAD_COMMIT_LIMIT` that limits the amount of commits is sent for each webhook payload, this was previously done via `[ui].FEED_MAX_COMMIT_NUM` which feels incorrect.
- The default is 15 for this new option, purely arbitary.
- Resolves forgejo/forgejo#6780
- Added unit testing, it's quite a lot because this the notification
area is not really easy to test and rather should've been a integration test
but that ends up having more complicated than trying doing an unit test.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6797
Reviewed-by: Otto <otto@codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>

authored by

Gusted
Gusted
and committed by
Gusted
94845020 93f84db5

+268 -18
+17 -14
modules/setting/webhook.go
··· 11 11 12 12 // Webhook settings 13 13 var Webhook = struct { 14 - QueueLength int 15 - DeliverTimeout int 16 - SkipTLSVerify bool 17 - AllowedHostList string 18 - PagingNum int 19 - ProxyURL string 20 - ProxyURLFixed *url.URL 21 - ProxyHosts []string 14 + QueueLength int 15 + DeliverTimeout int 16 + SkipTLSVerify bool 17 + AllowedHostList string 18 + PagingNum int 19 + ProxyURL string 20 + ProxyURLFixed *url.URL 21 + ProxyHosts []string 22 + PayloadCommitLimit int 22 23 }{ 23 - QueueLength: 1000, 24 - DeliverTimeout: 5, 25 - SkipTLSVerify: false, 26 - PagingNum: 10, 27 - ProxyURL: "", 28 - ProxyHosts: []string{}, 24 + QueueLength: 1000, 25 + DeliverTimeout: 5, 26 + SkipTLSVerify: false, 27 + PagingNum: 10, 28 + ProxyURL: "", 29 + ProxyHosts: []string{}, 30 + PayloadCommitLimit: 15, 29 31 } 30 32 31 33 func loadWebhookFrom(rootCfg ConfigProvider) { ··· 45 47 } 46 48 } 47 49 Webhook.ProxyHosts = sec.Key("PROXY_HOSTS").Strings(",") 50 + Webhook.PayloadCommitLimit = sec.Key("PAYLOAD_COMMIT_LIMIT").MustInt(15) 48 51 }
+9
services/feed/action.go
··· 17 17 "code.gitea.io/gitea/modules/json" 18 18 "code.gitea.io/gitea/modules/log" 19 19 "code.gitea.io/gitea/modules/repository" 20 + "code.gitea.io/gitea/modules/setting" 20 21 "code.gitea.io/gitea/modules/util" 21 22 notify_service "code.gitea.io/gitea/services/notify" 22 23 ) ··· 319 320 } 320 321 321 322 func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) { 323 + if len(commits.Commits) > setting.UI.FeedMaxCommitNum { 324 + commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] 325 + } 326 + 322 327 data, err := json.Marshal(commits) 323 328 if err != nil { 324 329 log.Error("Marshal: %v", err) ··· 390 395 } 391 396 392 397 func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) { 398 + if len(commits.Commits) > setting.UI.FeedMaxCommitNum { 399 + commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] 400 + } 401 + 393 402 data, err := json.Marshal(commits) 394 403 if err != nil { 395 404 log.Error("json.Marshal: %v", err)
+91
services/feed/action_test.go
··· 12 12 repo_model "code.gitea.io/gitea/models/repo" 13 13 "code.gitea.io/gitea/models/unittest" 14 14 user_model "code.gitea.io/gitea/models/user" 15 + "code.gitea.io/gitea/modules/git" 16 + "code.gitea.io/gitea/modules/repository" 17 + "code.gitea.io/gitea/modules/setting" 18 + "code.gitea.io/gitea/modules/test" 15 19 16 20 _ "code.gitea.io/gitea/models/actions" 17 21 _ "code.gitea.io/gitea/models/forgefed" 18 22 23 + "github.com/stretchr/testify/assert" 19 24 "github.com/stretchr/testify/require" 20 25 ) 21 26 ··· 51 56 unittest.AssertExistsAndLoadBean(t, actionBean) 52 57 unittest.CheckConsistencyFor(t, &activities_model.Action{}) 53 58 } 59 + 60 + func pushCommits() *repository.PushCommits { 61 + pushCommits := repository.NewPushCommits() 62 + pushCommits.Commits = []*repository.PushCommit{ 63 + { 64 + Sha1: "69554a6", 65 + CommitterEmail: "user2@example.com", 66 + CommitterName: "User2", 67 + AuthorEmail: "user2@example.com", 68 + AuthorName: "User2", 69 + Message: "not signed commit", 70 + }, 71 + { 72 + Sha1: "27566bd", 73 + CommitterEmail: "user2@example.com", 74 + CommitterName: "User2", 75 + AuthorEmail: "user2@example.com", 76 + AuthorName: "User2", 77 + Message: "good signed commit (with not yet validated email)", 78 + }, 79 + { 80 + Sha1: "5099b81", 81 + CommitterEmail: "user2@example.com", 82 + CommitterName: "User2", 83 + AuthorEmail: "user2@example.com", 84 + AuthorName: "User2", 85 + Message: "good signed commit", 86 + }, 87 + } 88 + pushCommits.HeadCommit = &repository.PushCommit{Sha1: "69554a6"} 89 + return pushCommits 90 + } 91 + 92 + func TestSyncPushCommits(t *testing.T) { 93 + require.NoError(t, unittest.PrepareTestDatabase()) 94 + 95 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 96 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID}) 97 + 98 + t.Run("All commits", func(t *testing.T) { 99 + defer test.MockVariableValue(&setting.UI.FeedMaxCommitNum, 10)() 100 + 101 + maxID := unittest.GetCount(t, &activities_model.Action{}) 102 + NewNotifier().SyncPushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("master")}, pushCommits()) 103 + 104 + newNotification := unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ActUserID: user.ID, RefName: "refs/heads/master"}, unittest.Cond("id > ?", maxID)) 105 + assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"27566bd","Message":"good signed commit (with not yet validated email)","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"5099b81","Message":"good signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content) 106 + }) 107 + 108 + t.Run("Only one commit", func(t *testing.T) { 109 + defer test.MockVariableValue(&setting.UI.FeedMaxCommitNum, 1)() 110 + 111 + maxID := unittest.GetCount(t, &activities_model.Action{}) 112 + NewNotifier().SyncPushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("main")}, pushCommits()) 113 + 114 + newNotification := unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ActUserID: user.ID, RefName: "refs/heads/main"}, unittest.Cond("id > ?", maxID)) 115 + assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content) 116 + }) 117 + } 118 + 119 + func TestPushCommits(t *testing.T) { 120 + require.NoError(t, unittest.PrepareTestDatabase()) 121 + 122 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 123 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID}) 124 + 125 + t.Run("All commits", func(t *testing.T) { 126 + defer test.MockVariableValue(&setting.UI.FeedMaxCommitNum, 10)() 127 + 128 + maxID := unittest.GetCount(t, &activities_model.Action{}) 129 + NewNotifier().PushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("master")}, pushCommits()) 130 + 131 + newNotification := unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ActUserID: user.ID, RefName: "refs/heads/master"}, unittest.Cond("id > ?", maxID)) 132 + assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"27566bd","Message":"good signed commit (with not yet validated email)","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"5099b81","Message":"good signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content) 133 + }) 134 + 135 + t.Run("Only one commit", func(t *testing.T) { 136 + defer test.MockVariableValue(&setting.UI.FeedMaxCommitNum, 1)() 137 + 138 + maxID := unittest.GetCount(t, &activities_model.Action{}) 139 + NewNotifier().PushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("main")}, pushCommits()) 140 + 141 + newNotification := unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ActUserID: user.ID, RefName: "refs/heads/main"}, unittest.Cond("id > ?", maxID)) 142 + assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content) 143 + }) 144 + }
-4
services/repository/push.go
··· 252 252 commits.CompareURL = "" 253 253 } 254 254 255 - if len(commits.Commits) > setting.UI.FeedMaxCommitNum { 256 - commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] 257 - } 258 - 259 255 notify_service.PushCommits(ctx, pusher, repo, opts, commits) 260 256 261 257 // Cache for big repository
+9
services/webhook/TestPushCommits/webhook.yml
··· 1 + - 2 + id: 1001 3 + repo_id: 2 4 + type: forgejo 5 + url: http://www.example.com/blåhaj 6 + http_method: POST 7 + content_type: 1 # json 8 + events: '{"send_everything":true,"branch_filter":"{master*,main*}"}' 9 + is_active: true
+8
services/webhook/notifier.go
··· 599 599 } 600 600 601 601 func (m *webhookNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) { 602 + if len(commits.Commits) > setting.Webhook.PayloadCommitLimit { 603 + commits.Commits = commits.Commits[:setting.Webhook.PayloadCommitLimit] 604 + } 605 + 602 606 apiPusher := convert.ToUser(ctx, pusher, nil) 603 607 apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo.RepoPath(), repo.HTMLURL()) 604 608 if err != nil { ··· 840 844 } 841 845 842 846 func (m *webhookNotifier) SyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) { 847 + if len(commits.Commits) > setting.Webhook.PayloadCommitLimit { 848 + commits.Commits = commits.Commits[:setting.Webhook.PayloadCommitLimit] 849 + } 850 + 843 851 apiPusher := convert.ToUser(ctx, pusher, nil) 844 852 apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo.RepoPath(), repo.HTMLURL()) 845 853 if err != nil {
+134
services/webhook/notifier_test.go
··· 1 + // Copyright 2025 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + package webhook 5 + 6 + import ( 7 + "path/filepath" 8 + "testing" 9 + 10 + "code.gitea.io/gitea/models/db" 11 + repo_model "code.gitea.io/gitea/models/repo" 12 + "code.gitea.io/gitea/models/unittest" 13 + user_model "code.gitea.io/gitea/models/user" 14 + webhook_model "code.gitea.io/gitea/models/webhook" 15 + "code.gitea.io/gitea/modules/git" 16 + "code.gitea.io/gitea/modules/json" 17 + "code.gitea.io/gitea/modules/repository" 18 + "code.gitea.io/gitea/modules/setting" 19 + "code.gitea.io/gitea/modules/structs" 20 + "code.gitea.io/gitea/modules/test" 21 + 22 + "github.com/stretchr/testify/assert" 23 + "github.com/stretchr/testify/require" 24 + ) 25 + 26 + func pushCommits() *repository.PushCommits { 27 + pushCommits := repository.NewPushCommits() 28 + pushCommits.Commits = []*repository.PushCommit{ 29 + { 30 + Sha1: "2c54faec6c45d31c1abfaecdab471eac6633738a", 31 + CommitterEmail: "user2@example.com", 32 + CommitterName: "User2", 33 + AuthorEmail: "user2@example.com", 34 + AuthorName: "User2", 35 + Message: "not signed commit", 36 + }, 37 + { 38 + Sha1: "205ac761f3326a7ebe416e8673760016450b5cec", 39 + CommitterEmail: "user2@example.com", 40 + CommitterName: "User2", 41 + AuthorEmail: "user2@example.com", 42 + AuthorName: "User2", 43 + Message: "good signed commit (with not yet validated email)", 44 + }, 45 + { 46 + Sha1: "1032bbf17fbc0d9c95bb5418dabe8f8c99278700", 47 + CommitterEmail: "user2@example.com", 48 + CommitterName: "User2", 49 + AuthorEmail: "user2@example.com", 50 + AuthorName: "User2", 51 + Message: "good signed commit", 52 + }, 53 + } 54 + pushCommits.HeadCommit = &repository.PushCommit{Sha1: "2c54faec6c45d31c1abfaecdab471eac6633738a"} 55 + return pushCommits 56 + } 57 + 58 + func TestSyncPushCommits(t *testing.T) { 59 + defer unittest.OverrideFixtures( 60 + unittest.FixturesOptions{ 61 + Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"), 62 + Base: setting.AppWorkPath, 63 + Dirs: []string{"services/webhook/TestPushCommits"}, 64 + }, 65 + )() 66 + require.NoError(t, unittest.PrepareTestDatabase()) 67 + 68 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 69 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: user.ID}) 70 + 71 + t.Run("All commits", func(t *testing.T) { 72 + defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)() 73 + 74 + NewNotifier().SyncPushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("master-1")}, pushCommits()) 75 + 76 + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("payload_content LIKE '%master-1%'")) 77 + 78 + var payloadContent structs.PushPayload 79 + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) 80 + assert.Len(t, payloadContent.Commits, 3) 81 + }) 82 + 83 + t.Run("Only one commit", func(t *testing.T) { 84 + defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 1)() 85 + 86 + NewNotifier().SyncPushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("main-1")}, pushCommits()) 87 + 88 + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("payload_content LIKE '%main-1%'")) 89 + 90 + var payloadContent structs.PushPayload 91 + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) 92 + assert.Len(t, payloadContent.Commits, 1) 93 + assert.EqualValues(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID) 94 + }) 95 + } 96 + 97 + func TestPushCommits(t *testing.T) { 98 + defer unittest.OverrideFixtures( 99 + unittest.FixturesOptions{ 100 + Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"), 101 + Base: setting.AppWorkPath, 102 + Dirs: []string{"services/webhook/TestPushCommits"}, 103 + }, 104 + )() 105 + require.NoError(t, unittest.PrepareTestDatabase()) 106 + 107 + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 108 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: user.ID}) 109 + 110 + t.Run("All commits", func(t *testing.T) { 111 + defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)() 112 + 113 + NewNotifier().PushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("master-2")}, pushCommits()) 114 + 115 + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("payload_content LIKE '%master-2%'")) 116 + 117 + var payloadContent structs.PushPayload 118 + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) 119 + assert.Len(t, payloadContent.Commits, 3) 120 + }) 121 + 122 + t.Run("Only one commit", func(t *testing.T) { 123 + defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 1)() 124 + 125 + NewNotifier().PushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("main-2")}, pushCommits()) 126 + 127 + hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("payload_content LIKE '%main-2%'")) 128 + 129 + var payloadContent structs.PushPayload 130 + require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent)) 131 + assert.Len(t, payloadContent.Commits, 1) 132 + assert.EqualValues(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID) 133 + }) 134 + }