Skip to content

Commit 1915361

Browse files
committed
Add webhook payload size optimization options
1 parent 3531e9d commit 1915361

File tree

10 files changed

+240
-0
lines changed

10 files changed

+240
-0
lines changed

models/migrations/migrations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ func prepareMigrationTasks() []*migration {
382382
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
383383
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
384384
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),
385+
newMigration(321, "Add webhook payload optimization columns", v1_24.AddWebhookPayloadOptimizationColumns),
385386
}
386387
return preparedMigrations
387388
}

models/migrations/v1_24/v321.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_24
5+
6+
import (
7+
"xorm.io/xorm"
8+
)
9+
10+
func AddWebhookPayloadOptimizationColumns(x *xorm.Engine) error {
11+
type Webhook struct {
12+
ExcludeFiles bool `xorm:"exclude_files NOT NULL DEFAULT false"`
13+
ExcludeCommits bool `xorm:"exclude_commits NOT NULL DEFAULT false"`
14+
}
15+
_, err := x.SyncWithOptions(
16+
xorm.SyncOptions{
17+
IgnoreConstrains: true,
18+
IgnoreIndices: true,
19+
},
20+
new(Webhook),
21+
)
22+
return err
23+
}

models/webhook/webhook.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ type Webhook struct {
139139
// HeaderAuthorizationEncrypted should be accessed using HeaderAuthorization() and SetHeaderAuthorization()
140140
HeaderAuthorizationEncrypted string `xorm:"TEXT"`
141141

142+
// Payload size optimization options
143+
ExcludeFiles bool `xorm:"exclude_files"` // Exclude file changes from commit payloads
144+
ExcludeCommits bool `xorm:"exclude_commits"` // Exclude commits and head_commit from push payloads
145+
142146
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
143147
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
144148
}

models/webhook/webhook_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,45 @@ func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *test
330330
assert.NoError(t, CleanupHookTaskTable(t.Context(), OlderThan, 168*time.Hour, 0))
331331
unittest.AssertExistsAndLoadBean(t, hookTask)
332332
}
333+
334+
func TestWebhookPayloadOptimization(t *testing.T) {
335+
assert.NoError(t, unittest.PrepareTestDatabase())
336+
337+
webhook := &Webhook{
338+
RepoID: 1,
339+
URL: "http://example.com/webhook",
340+
HTTPMethod: "POST",
341+
ContentType: ContentTypeJSON,
342+
Secret: "secret",
343+
IsActive: true,
344+
Type: webhook_module.GITEA,
345+
ExcludeFiles: true,
346+
ExcludeCommits: false,
347+
HookEvent: &webhook_module.HookEvent{
348+
PushOnly: true,
349+
},
350+
}
351+
352+
// Test creating webhook with payload optimization options
353+
err := CreateWebhook(db.DefaultContext, webhook)
354+
assert.NoError(t, err)
355+
assert.NotZero(t, webhook.ID)
356+
357+
// Test retrieving webhook and checking payload optimization options
358+
retrievedWebhook, err := GetWebhookByID(db.DefaultContext, webhook.ID)
359+
assert.NoError(t, err)
360+
assert.True(t, retrievedWebhook.ExcludeFiles)
361+
assert.False(t, retrievedWebhook.ExcludeCommits)
362+
363+
// Test updating webhook with different payload optimization options
364+
retrievedWebhook.ExcludeFiles = false
365+
retrievedWebhook.ExcludeCommits = true
366+
err = UpdateWebhook(db.DefaultContext, retrievedWebhook)
367+
assert.NoError(t, err)
368+
369+
// Verify the update
370+
updatedWebhook, err := GetWebhookByID(db.DefaultContext, webhook.ID)
371+
assert.NoError(t, err)
372+
assert.False(t, updatedWebhook.ExcludeFiles)
373+
assert.True(t, updatedWebhook.ExcludeCommits)
374+
}

options/locale/locale_en-US.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,6 +2424,11 @@ settings.event_package = Package
24242424
settings.event_package_desc = Package created or deleted in a repository.
24252425
settings.branch_filter = Branch filter
24262426
settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or <code>*</code>, events for all branches are reported. See <a href="%[1]s">%[2]s</a> documentation for syntax. Examples: <code>master</code>, <code>{master,release*}</code>.
2427+
settings.payload_optimization = Payload Size Optimization
2428+
settings.exclude_files = Exclude file changes
2429+
settings.exclude_files_desc = Remove file information (added, removed, modified files) from commit payloads to reduce webhook size.
2430+
settings.exclude_commits = Exclude commits
2431+
settings.exclude_commits_desc = Remove commits and head_commit from push payloads, keeping only before, after and total_commits fields.
24272432
settings.authorization_header = Authorization Header
24282433
settings.authorization_header_desc = Will be included as authorization header for requests when present. Examples: %s.
24292434
settings.active = Active

routers/web/repo/setting/webhook.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ func createWebhook(ctx *context.Context, params webhookParams) {
243243
Meta: string(meta),
244244
OwnerID: orCtx.OwnerID,
245245
IsSystemWebhook: orCtx.IsSystemWebhook,
246+
ExcludeFiles: params.WebhookForm.ExcludeFiles,
247+
ExcludeCommits: params.WebhookForm.ExcludeCommits,
246248
}
247249
err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
248250
if err != nil {
@@ -294,6 +296,8 @@ func editWebhook(ctx *context.Context, params webhookParams) {
294296
w.IsActive = params.WebhookForm.Active
295297
w.HTTPMethod = params.HTTPMethod
296298
w.Meta = string(meta)
299+
w.ExcludeFiles = params.WebhookForm.ExcludeFiles
300+
w.ExcludeCommits = params.WebhookForm.ExcludeCommits
297301

298302
err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
299303
if err != nil {

services/forms/repo_form.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ type WebhookForm struct {
239239
BranchFilter string `binding:"GlobPattern"`
240240
AuthorizationHeader string
241241
Secret string
242+
// Payload size optimization options
243+
ExcludeFiles bool // Exclude file changes from commit payloads
244+
ExcludeCommits bool // Exclude commits and head_commit from push payloads
242245
}
243246

244247
// PushOnly if the hook will be triggered when push

services/webhook/notifier.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88

99
actions_model "code.gitea.io/gitea/models/actions"
10+
"code.gitea.io/gitea/models/db"
1011
git_model "code.gitea.io/gitea/models/git"
1112
issues_model "code.gitea.io/gitea/models/issues"
1213
"code.gitea.io/gitea/models/organization"
@@ -15,10 +16,12 @@ import (
1516
access_model "code.gitea.io/gitea/models/perm/access"
1617
repo_model "code.gitea.io/gitea/models/repo"
1718
user_model "code.gitea.io/gitea/models/user"
19+
webhook_model "code.gitea.io/gitea/models/webhook"
1820
"code.gitea.io/gitea/modules/git"
1921
"code.gitea.io/gitea/modules/gitrepo"
2022
"code.gitea.io/gitea/modules/httplib"
2123
"code.gitea.io/gitea/modules/log"
24+
"code.gitea.io/gitea/modules/optional"
2225
"code.gitea.io/gitea/modules/repository"
2326
"code.gitea.io/gitea/modules/setting"
2427
api "code.gitea.io/gitea/modules/structs"
@@ -640,6 +643,57 @@ func (m *webhookNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
640643
}
641644
}
642645

646+
// applyWebhookPayloadOptimizations applies payload size optimizations based on webhook configurations
647+
func (m *webhookNotifier) applyWebhookPayloadOptimizations(ctx context.Context, repo *repo_model.Repository, apiCommits []*api.PayloadCommit, apiHeadCommit *api.PayloadCommit) ([]*api.PayloadCommit, *api.PayloadCommit) {
648+
// Get webhooks for this repository to check their configuration
649+
webhooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
650+
RepoID: repo.ID,
651+
IsActive: optional.Some(true),
652+
})
653+
if err != nil {
654+
log.Error("Failed to get webhooks for repository %d: %v", repo.ID, err)
655+
// Continue with default behavior if we can't get webhooks
656+
return apiCommits, apiHeadCommit
657+
}
658+
659+
// Check if any webhook has payload optimization options enabled
660+
hasExcludeFiles := false
661+
hasExcludeCommits := false
662+
for _, webhook := range webhooks {
663+
if webhook.HasEvent(webhook_module.HookEventPush) {
664+
if webhook.ExcludeFiles {
665+
hasExcludeFiles = true
666+
}
667+
if webhook.ExcludeCommits {
668+
hasExcludeCommits = true
669+
}
670+
}
671+
}
672+
673+
// Apply payload optimizations based on webhook configurations
674+
if hasExcludeFiles {
675+
// Remove file information from commits
676+
for _, commit := range apiCommits {
677+
commit.Added = nil
678+
commit.Removed = nil
679+
commit.Modified = nil
680+
}
681+
if apiHeadCommit != nil {
682+
apiHeadCommit.Added = nil
683+
apiHeadCommit.Removed = nil
684+
apiHeadCommit.Modified = nil
685+
}
686+
}
687+
688+
if hasExcludeCommits {
689+
// Exclude commits and head_commit from payload
690+
apiCommits = nil
691+
apiHeadCommit = nil
692+
}
693+
694+
return apiCommits, apiHeadCommit
695+
}
696+
643697
func (m *webhookNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
644698
apiPusher := convert.ToUser(ctx, pusher, nil)
645699
apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo)
@@ -648,6 +702,9 @@ func (m *webhookNotifier) PushCommits(ctx context.Context, pusher *user_model.Us
648702
return
649703
}
650704

705+
// Apply payload optimizations
706+
apiCommits, apiHeadCommit = m.applyWebhookPayloadOptimizations(ctx, repo, apiCommits, apiHeadCommit)
707+
651708
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{
652709
Ref: opts.RefFullName.String(),
653710
Before: opts.OldCommitID,
@@ -887,6 +944,9 @@ func (m *webhookNotifier) SyncPushCommits(ctx context.Context, pusher *user_mode
887944
return
888945
}
889946

947+
// Apply payload optimizations
948+
apiCommits, apiHeadCommit = m.applyWebhookPayloadOptimizations(ctx, repo, apiCommits, apiHeadCommit)
949+
890950
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{
891951
Ref: opts.RefFullName.String(),
892952
Before: opts.OldCommitID,

services/webhook/webhook_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,82 @@ func TestWebhookUserMail(t *testing.T) {
9191
assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email)
9292
assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email)
9393
}
94+
95+
func TestWebhookPayloadOptimization(t *testing.T) {
96+
assert.NoError(t, unittest.PrepareTestDatabase())
97+
98+
// Create a test repository
99+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
100+
101+
// Create a webhook with payload optimization enabled
102+
webhook := &webhook_model.Webhook{
103+
RepoID: repo.ID,
104+
URL: "http://example.com/webhook",
105+
HTTPMethod: "POST",
106+
ContentType: webhook_model.ContentTypeJSON,
107+
Secret: "secret",
108+
IsActive: true,
109+
Type: webhook_module.GITEA,
110+
ExcludeFiles: true,
111+
ExcludeCommits: false,
112+
HookEvent: &webhook_module.HookEvent{
113+
PushOnly: true,
114+
},
115+
}
116+
117+
err := webhook_model.CreateWebhook(db.DefaultContext, webhook)
118+
assert.NoError(t, err)
119+
120+
// Create test commits with file information
121+
apiCommits := []*api.PayloadCommit{
122+
{
123+
ID: "abc123",
124+
Message: "Test commit",
125+
Added: []string{"file1.txt", "file2.txt"},
126+
Removed: []string{"oldfile.txt"},
127+
Modified: []string{"modified.txt"},
128+
},
129+
{
130+
ID: "def456",
131+
Message: "Another commit",
132+
Added: []string{"file3.txt"},
133+
Removed: []string{},
134+
Modified: []string{"file1.txt"},
135+
},
136+
}
137+
138+
apiHeadCommit := &api.PayloadCommit{
139+
ID: "def456",
140+
Message: "Another commit",
141+
Added: []string{"file3.txt"},
142+
Removed: []string{},
143+
Modified: []string{"file1.txt"},
144+
}
145+
146+
// Test payload optimization
147+
notifier := &webhookNotifier{}
148+
optimizedCommits, optimizedHeadCommit := notifier.applyWebhookPayloadOptimizations(db.DefaultContext, repo, apiCommits, apiHeadCommit)
149+
150+
// Verify that file information was removed when ExcludeFiles is true
151+
assert.Nil(t, optimizedCommits[0].Added)
152+
assert.Nil(t, optimizedCommits[0].Removed)
153+
assert.Nil(t, optimizedCommits[0].Modified)
154+
assert.Nil(t, optimizedCommits[1].Added)
155+
assert.Nil(t, optimizedCommits[1].Removed)
156+
assert.Nil(t, optimizedCommits[1].Modified)
157+
assert.Nil(t, optimizedHeadCommit.Added)
158+
assert.Nil(t, optimizedHeadCommit.Removed)
159+
assert.Nil(t, optimizedHeadCommit.Modified)
160+
161+
// Test with ExcludeCommits enabled
162+
webhook.ExcludeFiles = false
163+
webhook.ExcludeCommits = true
164+
err = webhook_model.UpdateWebhook(db.DefaultContext, webhook)
165+
assert.NoError(t, err)
166+
167+
optimizedCommits, optimizedHeadCommit = notifier.applyWebhookPayloadOptimizations(db.DefaultContext, repo, apiCommits, apiHeadCommit)
168+
169+
// Verify that commits and head_commit were excluded
170+
assert.Nil(t, optimizedCommits)
171+
assert.Nil(t, optimizedHeadCommit)
172+
}

templates/repo/settings/webhook/settings.tmpl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@
4747
<span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc" "https://pkg.go.dev/github.com/gobwas/glob#Compile" "github.com/gobwas/glob"}}</span>
4848
</div>
4949

50+
<!-- Payload size optimization options -->
51+
<div class="field">
52+
<h4>{{ctx.Locale.Tr "repo.settings.payload_optimization"}}</h4>
53+
<div class="field">
54+
<div class="ui checkbox">
55+
<input name="exclude_files" type="checkbox" {{if .Webhook.ExcludeFiles}}checked{{end}}>
56+
<label>{{ctx.Locale.Tr "repo.settings.exclude_files"}}</label>
57+
<span class="help">{{ctx.Locale.Tr "repo.settings.exclude_files_desc"}}</span>
58+
</div>
59+
</div>
60+
<div class="field">
61+
<div class="ui checkbox">
62+
<input name="exclude_commits" type="checkbox" {{if .Webhook.ExcludeCommits}}checked{{end}}>
63+
<label>{{ctx.Locale.Tr "repo.settings.exclude_commits"}}</label>
64+
<span class="help">{{ctx.Locale.Tr "repo.settings.exclude_commits_desc"}}</span>
65+
</div>
66+
</div>
67+
</div>
68+
5069
<div class="field">
5170
<h4>{{ctx.Locale.Tr "repo.settings.event_desc"}}</h4>
5271
<div class="grouped event type fields">

0 commit comments

Comments
 (0)