diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 4f899453b5f57..a6b90c0dacc87 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -383,9 +383,9 @@ func prepareMigrationTasks() []*migration {
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),
-
// Gitea 1.24.0 ends at database version 321
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
+ newMigration(322, "Add webhook payload optimization JSON field", v1_25.AddWebhookPayloadOptimizationColumns),
}
return preparedMigrations
}
diff --git a/models/migrations/v1_25/v322.go b/models/migrations/v1_25/v322.go
new file mode 100644
index 0000000000000..c1e99bb0e53a8
--- /dev/null
+++ b/models/migrations/v1_25/v322.go
@@ -0,0 +1,22 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_25
+
+import (
+ "xorm.io/xorm"
+)
+
+func AddWebhookPayloadOptimizationColumns(x *xorm.Engine) error {
+ type Webhook struct {
+ MetaSettings string `xorm:"meta_settings TEXT"`
+ }
+ _, err := x.SyncWithOptions(
+ xorm.SyncOptions{
+ IgnoreConstrains: true,
+ IgnoreIndices: true,
+ },
+ new(Webhook),
+ )
+ return err
+}
diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go
index 7d4b2e2237db0..423557f5b9ae4 100644
--- a/models/webhook/webhook.go
+++ b/models/webhook/webhook.go
@@ -22,6 +22,38 @@ import (
"xorm.io/builder"
)
+// MetaSettings represents the metadata settings for webhook
+type MetaSettings struct {
+ PayloadOptimization *PayloadOptimizationConfig `json:"payload_optimization,omitempty"` // Payload optimization configuration
+}
+
+// PayloadOptimizationConfig represents the configuration for webhook payload optimization
+type PayloadOptimizationConfig struct {
+ Files *PayloadOptimizationItem `json:"files,omitempty"` // Files optimization config
+ Commits *PayloadOptimizationItem `json:"commits,omitempty"` // Commits optimization config
+}
+
+// PayloadOptimizationItem represents a single optimization item configuration
+type PayloadOptimizationItem struct {
+ Enable bool `json:"enable"` // Whether to enable optimization for this item
+ Limit int `json:"limit"` // 0: trim all (none kept), >0: keep N items (forward order), <0: keep N items (reverse order)
+}
+
+// DefaultMetaSettings returns the default webhook meta settings
+func DefaultMetaSettings() *MetaSettings {
+ return &MetaSettings{
+ PayloadOptimization: DefaultPayloadOptimizationConfig(),
+ }
+}
+
+// DefaultPayloadOptimizationConfig returns the default payload optimization configuration
+func DefaultPayloadOptimizationConfig() *PayloadOptimizationConfig {
+ return &PayloadOptimizationConfig{
+ Files: &PayloadOptimizationItem{Enable: false, Limit: 0},
+ Commits: &PayloadOptimizationItem{Enable: false, Limit: 0},
+ }
+}
+
// ErrWebhookNotExist represents a "WebhookNotExist" kind of error.
type ErrWebhookNotExist struct {
ID int64
@@ -139,6 +171,9 @@ type Webhook struct {
// HeaderAuthorizationEncrypted should be accessed using HeaderAuthorization() and SetHeaderAuthorization()
HeaderAuthorizationEncrypted string `xorm:"TEXT"`
+ // Webhook metadata settings (JSON format)
+ MetaSettings string `xorm:"meta_settings TEXT"` // JSON: webhook metadata configuration
+
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
@@ -346,3 +381,95 @@ func DeleteWebhookByOwnerID(ctx context.Context, ownerID, id int64) error {
}
return DeleteWebhookByID(ctx, id)
}
+
+// GetMetaSettings returns the webhook meta settings
+func (w *Webhook) GetMetaSettings() *MetaSettings {
+ if w.MetaSettings == "" {
+ return DefaultMetaSettings()
+ }
+
+ var settings MetaSettings
+ if err := json.Unmarshal([]byte(w.MetaSettings), &settings); err != nil {
+ log.Error("Failed to unmarshal webhook meta settings: %v", err)
+ return DefaultMetaSettings()
+ }
+
+ // Ensure payload optimization config is initialized
+ if settings.PayloadOptimization == nil {
+ settings.PayloadOptimization = DefaultPayloadOptimizationConfig()
+ }
+
+ return &settings
+}
+
+// GetPayloadOptimizationConfig returns the payload optimization configuration
+func (w *Webhook) GetPayloadOptimizationConfig() *PayloadOptimizationConfig {
+ return w.GetMetaSettings().PayloadOptimization
+}
+
+// SetMetaSettings sets the webhook meta settings
+func (w *Webhook) SetMetaSettings(settings *MetaSettings) error {
+ if settings == nil {
+ settings = DefaultMetaSettings()
+ }
+
+ data, err := json.Marshal(settings)
+ if err != nil {
+ return fmt.Errorf("failed to marshal webhook meta settings: %w", err)
+ }
+
+ w.MetaSettings = string(data)
+ return nil
+}
+
+// SetPayloadOptimizationConfig sets the payload optimization configuration
+func (w *Webhook) SetPayloadOptimizationConfig(config *PayloadOptimizationConfig) error {
+ settings := w.GetMetaSettings()
+ if config == nil {
+ config = DefaultPayloadOptimizationConfig()
+ }
+ settings.PayloadOptimization = config
+ return w.SetMetaSettings(settings)
+}
+
+// IsPayloadOptimizationEnabled returns whether payload optimization is enabled
+func (w *Webhook) IsPayloadOptimizationEnabled() bool {
+ config := w.GetPayloadOptimizationConfig()
+ return config.Files.Enable || config.Commits.Enable
+}
+
+// GetPayloadOptimizationLimit returns the payload optimization limit
+func (w *Webhook) GetPayloadOptimizationLimit() int {
+ config := w.GetPayloadOptimizationConfig()
+ if config.Files.Enable {
+ return config.Files.Limit
+ }
+ if config.Commits.Enable {
+ return config.Commits.Limit
+ }
+ return 0
+}
+
+// IsFilesOptimizationEnabled returns whether files optimization is enabled
+func (w *Webhook) IsFilesOptimizationEnabled() bool {
+ config := w.GetPayloadOptimizationConfig()
+ return config.Files.Enable
+}
+
+// GetFilesOptimizationLimit returns the files optimization limit
+func (w *Webhook) GetFilesOptimizationLimit() int {
+ config := w.GetPayloadOptimizationConfig()
+ return config.Files.Limit
+}
+
+// IsCommitsOptimizationEnabled returns whether commits optimization is enabled
+func (w *Webhook) IsCommitsOptimizationEnabled() bool {
+ config := w.GetPayloadOptimizationConfig()
+ return config.Commits.Enable
+}
+
+// GetCommitsOptimizationLimit returns the commits optimization limit
+func (w *Webhook) GetCommitsOptimizationLimit() int {
+ config := w.GetPayloadOptimizationConfig()
+ return config.Commits.Limit
+}
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
index edad8fc996c14..b6305a049c461 100644
--- a/models/webhook/webhook_test.go
+++ b/models/webhook/webhook_test.go
@@ -330,3 +330,63 @@ func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *test
assert.NoError(t, CleanupHookTaskTable(t.Context(), OlderThan, 168*time.Hour, 0))
unittest.AssertExistsAndLoadBean(t, hookTask)
}
+
+func TestWebhookPayloadOptimization(t *testing.T) {
+ webhook := &Webhook{}
+
+ // Test default configuration
+ config := webhook.GetPayloadOptimizationConfig()
+ assert.False(t, config.Files.Enable)
+ assert.Equal(t, 0, config.Files.Limit)
+ assert.False(t, config.Commits.Enable)
+ assert.Equal(t, 0, config.Commits.Limit)
+
+ // Test setting configuration via meta settings
+ metaSettings := &MetaSettings{
+ PayloadOptimization: &PayloadOptimizationConfig{
+ Files: &PayloadOptimizationItem{
+ Enable: true,
+ Limit: 5,
+ },
+ Commits: &PayloadOptimizationItem{
+ Enable: true,
+ Limit: -3,
+ },
+ },
+ }
+ webhook.SetMetaSettings(metaSettings)
+
+ // Test getting configuration
+ config = webhook.GetPayloadOptimizationConfig()
+ assert.True(t, config.Files.Enable)
+ assert.Equal(t, 5, config.Files.Limit)
+ assert.True(t, config.Commits.Enable)
+ assert.Equal(t, -3, config.Commits.Limit)
+
+ // Test individual methods
+ assert.True(t, webhook.IsFilesOptimizationEnabled())
+ assert.Equal(t, 5, webhook.GetFilesOptimizationLimit())
+ assert.True(t, webhook.IsCommitsOptimizationEnabled())
+ assert.Equal(t, -3, webhook.GetCommitsOptimizationLimit())
+ assert.True(t, webhook.IsPayloadOptimizationEnabled())
+
+ // Test backward compatibility with direct payload optimization config setting
+ newConfig := &PayloadOptimizationConfig{
+ Files: &PayloadOptimizationItem{
+ Enable: false,
+ Limit: 10,
+ },
+ Commits: &PayloadOptimizationItem{
+ Enable: false,
+ Limit: 20,
+ },
+ }
+ webhook.SetPayloadOptimizationConfig(newConfig)
+
+ // Verify the config is properly set through meta settings
+ config = webhook.GetPayloadOptimizationConfig()
+ assert.False(t, config.Files.Enable)
+ assert.Equal(t, 10, config.Files.Limit)
+ assert.False(t, config.Commits.Enable)
+ assert.Equal(t, 20, config.Commits.Limit)
+}
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index ac779a5740748..14e04d75f0a79 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -25,6 +25,8 @@ type Hook struct {
Events []string `json:"events"`
AuthorizationHeader string `json:"authorization_header"`
Active bool `json:"active"`
+ // MetaSettings webhook metadata settings including payload optimization
+ MetaSettings map[string]any `json:"meta_settings"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
// swagger:strfmt date-time
@@ -48,6 +50,8 @@ type CreateHookOption struct {
Events []string `json:"events"`
BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
AuthorizationHeader string `json:"authorization_header"`
+ // Webhook metadata settings including payload optimization
+ MetaSettings map[string]any `json:"meta_settings"` // {"payload_optimization": {"files": {"enable": bool, "limit": int}, "commits": {"enable": bool, "limit": int}}}
// default: false
Active bool `json:"active"`
}
@@ -58,7 +62,9 @@ type EditHookOption struct {
Events []string `json:"events"`
BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
AuthorizationHeader string `json:"authorization_header"`
- Active *bool `json:"active"`
+ // Webhook metadata settings including payload optimization
+ MetaSettings *map[string]any `json:"meta_settings"` // {"payload_optimization": {"files": {"enable": bool, "limit": int}, "commits": {"enable": bool, "limit": int}}}
+ Active *bool `json:"active"`
}
// Payloader payload is some part of one hook
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index d7e73a0cfbb08..d1777b83580e0 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2425,6 +2425,13 @@ settings.event_package = Package
settings.event_package_desc = Package created or deleted in a repository.
settings.branch_filter = Branch filter
settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or *
, events for all branches are reported. See %[2]s documentation for syntax. Examples: master
, {master,release*}
.
+settings.payload_optimization = Payload Size Optimization
+settings.payload_optimization_files = Files
+settings.payload_optimization_commits = Commits
+settings.payload_optimization_enable = Enable optimization
+settings.payload_optimization_enable_desc = Enable payload size optimization for this item
+settings.payload_optimization_limit = Limit
+settings.payload_optimization_limit_desc = 0: trim all (none kept), >0: keep N items (forward order), <0: keep N items (reverse order)
settings.authorization_header = Authorization Header
settings.authorization_header_desc = Will be included as authorization header for requests when present. Examples: %s.
settings.active = Active
@@ -3283,7 +3290,7 @@ auths.tip.github = Register a new OAuth application on %s
auths.tip.gitlab_new = Register a new application on %s
auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console at %s
auths.tip.openid_connect = Use the OpenID Connect Discovery URL "https://{server}/.well-known/openid-configuration" to specify the endpoints
-auths.tip.twitter = Go to %s, create an application and ensure that the “Allow this application to be used to Sign in with Twitter” option is enabled
+auths.tip.twitter = Go to %s, create an application and ensure that the "Allow this application to be used to Sign in with Twitter" option is enabled
auths.tip.discord = Register a new application on %s
auths.tip.gitea = Register a new OAuth2 application. Guide can be found at %s
auths.tip.yandex = Create a new application at %s. Select following permissions from the "Yandex.Passport API" section: "Access to email address", "Access to user avatar" and "Access to username, first name and surname, gender"
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 6f598f14c8b2b..fe12d976f015c 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -21,6 +21,37 @@ import (
webhook_service "code.gitea.io/gitea/services/webhook"
)
+// getBoolFromMap extracts a boolean value from a map with a default fallback
+//
+//nolint:unparam // defaultValue is needed for generic helper function
+func getBoolFromMap(m map[string]any, defaultValue bool) bool {
+ if val, ok := m["enable"]; ok {
+ if boolVal, ok := val.(bool); ok {
+ return boolVal
+ }
+ }
+ return defaultValue
+}
+
+// getIntFromMap extracts an integer value from a map with a default fallback
+//
+//nolint:unparam // defaultValue is needed for generic helper function
+func getIntFromMap(m map[string]any, defaultValue int) int {
+ if val, ok := m["limit"]; ok {
+ switch v := val.(type) {
+ case int:
+ return v
+ case float64:
+ return int(v)
+ case string:
+ if intVal, err := strconv.Atoi(v); err == nil {
+ return intVal
+ }
+ }
+ }
+ return defaultValue
+}
+
// ListOwnerHooks lists the webhooks of the provided owner
func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) {
opts := &webhook.ListWebhookOptions{
@@ -227,6 +258,44 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
IsActive: form.Active,
Type: form.Type,
}
+
+ // Set webhook meta settings
+ if form.MetaSettings != nil {
+ metaSettings := &webhook.MetaSettings{}
+
+ // Parse payload optimization config
+ if payloadOptMap, ok := form.MetaSettings["payload_optimization"].(map[string]any); ok {
+ payloadOptConfig := &webhook.PayloadOptimizationConfig{}
+
+ // Parse files config
+ if filesConfig, ok := payloadOptMap["files"].(map[string]any); ok {
+ payloadOptConfig.Files = &webhook.PayloadOptimizationItem{
+ Enable: getBoolFromMap(filesConfig, false),
+ Limit: getIntFromMap(filesConfig, 0),
+ }
+ } else {
+ payloadOptConfig.Files = &webhook.PayloadOptimizationItem{Enable: false, Limit: 0}
+ }
+
+ // Parse commits config
+ if commitsConfig, ok := payloadOptMap["commits"].(map[string]any); ok {
+ payloadOptConfig.Commits = &webhook.PayloadOptimizationItem{
+ Enable: getBoolFromMap(commitsConfig, false),
+ Limit: getIntFromMap(commitsConfig, 0),
+ }
+ } else {
+ payloadOptConfig.Commits = &webhook.PayloadOptimizationItem{Enable: false, Limit: 0}
+ }
+
+ metaSettings.PayloadOptimization = payloadOptConfig
+ }
+
+ if err := w.SetMetaSettings(metaSettings); err != nil {
+ ctx.APIErrorInternal(err)
+ return nil, false
+ }
+ }
+
err := w.SetHeaderAuthorization(form.AuthorizationHeader)
if err != nil {
ctx.APIErrorInternal(err)
@@ -391,6 +460,43 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.IsActive = *form.Active
}
+ // Update webhook meta settings
+ if form.MetaSettings != nil {
+ metaSettings := &webhook.MetaSettings{}
+
+ // Parse payload optimization config
+ if payloadOptMap, ok := (*form.MetaSettings)["payload_optimization"].(map[string]any); ok {
+ payloadOptConfig := &webhook.PayloadOptimizationConfig{}
+
+ // Parse files config
+ if filesConfig, ok := payloadOptMap["files"].(map[string]any); ok {
+ payloadOptConfig.Files = &webhook.PayloadOptimizationItem{
+ Enable: getBoolFromMap(filesConfig, false),
+ Limit: getIntFromMap(filesConfig, 0),
+ }
+ } else {
+ payloadOptConfig.Files = &webhook.PayloadOptimizationItem{Enable: false, Limit: 0}
+ }
+
+ // Parse commits config
+ if commitsConfig, ok := payloadOptMap["commits"].(map[string]any); ok {
+ payloadOptConfig.Commits = &webhook.PayloadOptimizationItem{
+ Enable: getBoolFromMap(commitsConfig, false),
+ Limit: getIntFromMap(commitsConfig, 0),
+ }
+ } else {
+ payloadOptConfig.Commits = &webhook.PayloadOptimizationItem{Enable: false, Limit: 0}
+ }
+
+ metaSettings.PayloadOptimization = payloadOptConfig
+ }
+
+ if err := w.SetMetaSettings(metaSettings); err != nil {
+ ctx.APIErrorInternal(err)
+ return false
+ }
+ }
+
if err := webhook.UpdateWebhook(ctx, w); err != nil {
ctx.APIErrorInternal(err)
return false
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index f107449749364..735c49f544b6b 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -244,6 +244,25 @@ func createWebhook(ctx *context.Context, params webhookParams) {
OwnerID: orCtx.OwnerID,
IsSystemWebhook: orCtx.IsSystemWebhook,
}
+
+ // Set webhook meta settings with payload optimization config
+ metaSettings := &webhook.MetaSettings{
+ PayloadOptimization: &webhook.PayloadOptimizationConfig{
+ Files: &webhook.PayloadOptimizationItem{
+ Enable: params.WebhookForm.PayloadOptimizationFilesEnable,
+ Limit: params.WebhookForm.PayloadOptimizationFilesLimit,
+ },
+ Commits: &webhook.PayloadOptimizationItem{
+ Enable: params.WebhookForm.PayloadOptimizationCommitsEnable,
+ Limit: params.WebhookForm.PayloadOptimizationCommitsLimit,
+ },
+ },
+ }
+ if err := w.SetMetaSettings(metaSettings); err != nil {
+ ctx.ServerError("SetMetaSettings", err)
+ return
+ }
+
err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
if err != nil {
ctx.ServerError("SetHeaderAuthorization", err)
@@ -295,6 +314,24 @@ func editWebhook(ctx *context.Context, params webhookParams) {
w.HTTPMethod = params.HTTPMethod
w.Meta = string(meta)
+ // Set webhook meta settings with payload optimization config
+ metaSettings := &webhook.MetaSettings{
+ PayloadOptimization: &webhook.PayloadOptimizationConfig{
+ Files: &webhook.PayloadOptimizationItem{
+ Enable: params.WebhookForm.PayloadOptimizationFilesEnable,
+ Limit: params.WebhookForm.PayloadOptimizationFilesLimit,
+ },
+ Commits: &webhook.PayloadOptimizationItem{
+ Enable: params.WebhookForm.PayloadOptimizationCommitsEnable,
+ Limit: params.WebhookForm.PayloadOptimizationCommitsLimit,
+ },
+ },
+ }
+ if err := w.SetMetaSettings(metaSettings); err != nil {
+ ctx.ServerError("SetMetaSettings", err)
+ return
+ }
+
err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
if err != nil {
ctx.ServerError("SetHeaderAuthorization", err)
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index cb267f891ccb7..57e7c98db5b4c 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -239,6 +239,11 @@ type WebhookForm struct {
BranchFilter string `binding:"GlobPattern"`
AuthorizationHeader string
Secret string
+ // Payload optimization settings
+ PayloadOptimizationFilesEnable bool `form:"payload_optimization_files_enable"`
+ PayloadOptimizationFilesLimit int `form:"payload_optimization_files_limit"`
+ PayloadOptimizationCommitsEnable bool `form:"payload_optimization_commits_enable"`
+ PayloadOptimizationCommitsLimit int `form:"payload_optimization_commits_limit"`
}
// PushOnly if the hook will be triggered when push
@@ -622,7 +627,7 @@ type UpdateAllowEditsForm struct {
// | _// __ \| | _/ __ \\__ \ / ___// __ \
// | | \ ___/| |_\ ___/ / __ \_\___ \\ ___/
// |____|_ /\___ >____/\___ >____ /____ >\___ >
-// \/ \/ \/ \/ \/ \/
+// \/ \/ \/ \/ \/ \/
// NewReleaseForm form for creating release
type NewReleaseForm struct {
diff --git a/services/webhook/general.go b/services/webhook/general.go
index be457e46f5f7a..d12d5224dbf34 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -409,6 +409,21 @@ func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
return nil, err
}
+ // Convert meta settings to map
+ metaSettings := w.GetMetaSettings()
+ metaSettingsMap := map[string]any{
+ "payload_optimization": map[string]any{
+ "files": map[string]any{
+ "enable": metaSettings.PayloadOptimization.Files.Enable,
+ "limit": metaSettings.PayloadOptimization.Files.Limit,
+ },
+ "commits": map[string]any{
+ "enable": metaSettings.PayloadOptimization.Commits.Enable,
+ "limit": metaSettings.PayloadOptimization.Commits.Limit,
+ },
+ },
+ }
+
return &api.Hook{
ID: w.ID,
Type: w.Type,
@@ -417,6 +432,7 @@ func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
Config: config,
Events: w.EventsArray(),
AuthorizationHeader: authorizationHeader,
+ MetaSettings: metaSettingsMap,
Updated: w.UpdatedUnix.AsTime(),
Created: w.CreatedUnix.AsTime(),
BranchFilter: w.BranchFilter,
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index 672abd5c95d0e..4939cd06a06fa 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -7,6 +7,7 @@ import (
"context"
actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
@@ -15,10 +16,12 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
+ webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@@ -640,6 +643,138 @@ func (m *webhookNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
}
}
+// applyWebhookPayloadOptimizations applies payload optimizations based on webhook configurations
+func (m *webhookNotifier) applyWebhookPayloadOptimizations(ctx context.Context, repo *repo_model.Repository, apiCommits []*api.PayloadCommit, apiHeadCommit *api.PayloadCommit) ([]*api.PayloadCommit, *api.PayloadCommit) {
+ // Get all webhooks for this repository
+ webhooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
+ RepoID: repo.ID,
+ IsActive: optional.Some(true),
+ })
+ if err != nil {
+ log.Error("Failed to get webhooks for repository %d: %v", repo.ID, err)
+ // Continue with default behavior if we can't get webhooks
+ return apiCommits, apiHeadCommit
+ }
+
+ // Check if any webhook has payload optimization options enabled
+ var filesLimit, commitsLimit int
+ hasFilesLimit := false
+ hasCommitsLimit := false
+ optimizationEnabled := false
+
+ for _, webhook := range webhooks {
+ if webhook.HasEvent(webhook_module.HookEventPush) {
+ config := webhook.GetPayloadOptimizationConfig()
+
+ // Check files optimization
+ if config.Files.Enable {
+ optimizationEnabled = true
+ if !hasFilesLimit || config.Files.Limit < filesLimit {
+ filesLimit = config.Files.Limit
+ hasFilesLimit = true
+ }
+ }
+
+ // Check commits optimization
+ if config.Commits.Enable {
+ optimizationEnabled = true
+ if !hasCommitsLimit || config.Commits.Limit < commitsLimit {
+ commitsLimit = config.Commits.Limit
+ hasCommitsLimit = true
+ }
+ }
+ }
+ }
+
+ // Apply payload optimizations based on webhook configurations
+ // 0: trim all (none kept), >0: trim to N items (forward order), <0: trim to N items (reverse order)
+ if optimizationEnabled {
+ // Apply files optimization to all commits
+ if hasFilesLimit {
+ for _, commit := range apiCommits {
+ if commit.Added != nil {
+ if filesLimit == 0 {
+ commit.Added = nil
+ } else if filesLimit > 0 && len(commit.Added) > filesLimit {
+ commit.Added = commit.Added[:filesLimit]
+ } else if filesLimit < 0 && len(commit.Added) > -filesLimit {
+ // Reverse order: keep the last N items
+ commit.Added = commit.Added[len(commit.Added)+filesLimit:]
+ }
+ }
+ if commit.Removed != nil {
+ if filesLimit == 0 {
+ commit.Removed = nil
+ } else if filesLimit > 0 && len(commit.Removed) > filesLimit {
+ commit.Removed = commit.Removed[:filesLimit]
+ } else if filesLimit < 0 && len(commit.Removed) > -filesLimit {
+ // Reverse order: keep the last N items
+ commit.Removed = commit.Removed[len(commit.Removed)+filesLimit:]
+ }
+ }
+ if commit.Modified != nil {
+ if filesLimit == 0 {
+ commit.Modified = nil
+ } else if filesLimit > 0 && len(commit.Modified) > filesLimit {
+ commit.Modified = commit.Modified[:filesLimit]
+ } else if filesLimit < 0 && len(commit.Modified) > -filesLimit {
+ // Reverse order: keep the last N items
+ commit.Modified = commit.Modified[len(commit.Modified)+filesLimit:]
+ }
+ }
+ }
+
+ // Apply files optimization to head commit
+ if apiHeadCommit != nil {
+ if apiHeadCommit.Added != nil {
+ if filesLimit == 0 {
+ apiHeadCommit.Added = nil
+ } else if filesLimit > 0 && len(apiHeadCommit.Added) > filesLimit {
+ apiHeadCommit.Added = apiHeadCommit.Added[:filesLimit]
+ } else if filesLimit < 0 && len(apiHeadCommit.Added) > -filesLimit {
+ // Reverse order: keep the last N items
+ apiHeadCommit.Added = apiHeadCommit.Added[len(apiHeadCommit.Added)+filesLimit:]
+ }
+ }
+ if apiHeadCommit.Removed != nil {
+ if filesLimit == 0 {
+ apiHeadCommit.Removed = nil
+ } else if filesLimit > 0 && len(apiHeadCommit.Removed) > filesLimit {
+ apiHeadCommit.Removed = apiHeadCommit.Removed[:filesLimit]
+ } else if filesLimit < 0 && len(apiHeadCommit.Removed) > -filesLimit {
+ // Reverse order: keep the last N items
+ apiHeadCommit.Removed = apiHeadCommit.Removed[len(apiHeadCommit.Removed)+filesLimit:]
+ }
+ }
+ if apiHeadCommit.Modified != nil {
+ if filesLimit == 0 {
+ apiHeadCommit.Modified = nil
+ } else if filesLimit > 0 && len(apiHeadCommit.Modified) > filesLimit {
+ apiHeadCommit.Modified = apiHeadCommit.Modified[:filesLimit]
+ } else if filesLimit < 0 && len(apiHeadCommit.Modified) > -filesLimit {
+ // Reverse order: keep the last N items
+ apiHeadCommit.Modified = apiHeadCommit.Modified[len(apiHeadCommit.Modified)+filesLimit:]
+ }
+ }
+ }
+ }
+
+ // Apply commits optimization
+ if hasCommitsLimit {
+ if commitsLimit == 0 {
+ apiCommits = nil
+ } else if commitsLimit > 0 && len(apiCommits) > commitsLimit {
+ apiCommits = apiCommits[:commitsLimit]
+ } else if commitsLimit < 0 && len(apiCommits) > -commitsLimit {
+ // Reverse order: keep the last N commits
+ apiCommits = apiCommits[len(apiCommits)+commitsLimit:]
+ }
+ }
+ }
+
+ return apiCommits, apiHeadCommit
+}
+
func (m *webhookNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
apiPusher := convert.ToUser(ctx, pusher, nil)
apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo)
@@ -648,6 +783,9 @@ func (m *webhookNotifier) PushCommits(ctx context.Context, pusher *user_model.Us
return
}
+ // Apply payload optimizations
+ apiCommits, apiHeadCommit = m.applyWebhookPayloadOptimizations(ctx, repo, apiCommits, apiHeadCommit)
+
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{
Ref: opts.RefFullName.String(),
Before: opts.OldCommitID,
@@ -887,6 +1025,9 @@ func (m *webhookNotifier) SyncPushCommits(ctx context.Context, pusher *user_mode
return
}
+ // Apply payload optimizations
+ apiCommits, apiHeadCommit = m.applyWebhookPayloadOptimizations(ctx, repo, apiCommits, apiHeadCommit)
+
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{
Ref: opts.RefFullName.String(),
Before: opts.OldCommitID,
diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go
index 5a805347e38a7..9a9477eeb9e61 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -91,3 +91,171 @@ func TestWebhookUserMail(t *testing.T) {
assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email)
assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email)
}
+
+func TestWebhookPayloadOptimization(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ // Create test webhook
+ webhook := &webhook_model.Webhook{
+ RepoID: repo.ID,
+ URL: "http://example.com/webhook",
+ HTTPMethod: "POST",
+ ContentType: webhook_model.ContentTypeJSON,
+ Secret: "secret",
+ IsActive: true,
+ Type: webhook_module.GITEA,
+ HookEvent: &webhook_module.HookEvent{
+ PushOnly: true,
+ },
+ }
+
+ // Test case 1: No optimization enabled
+ webhook.SetMetaSettings(&webhook_model.MetaSettings{
+ PayloadOptimization: &webhook_model.PayloadOptimizationConfig{
+ Files: &webhook_model.PayloadOptimizationItem{Enable: false, Limit: 0},
+ Commits: &webhook_model.PayloadOptimizationItem{Enable: false, Limit: 0},
+ },
+ })
+
+ err := webhook.UpdateEvent()
+ assert.NoError(t, err)
+ err = webhook_model.CreateWebhook(db.DefaultContext, webhook)
+ assert.NoError(t, err)
+
+ apiCommits := []*api.PayloadCommit{
+ {
+ ID: "abc123",
+ Message: "Test commit",
+ Added: []string{"file1.txt", "file2.txt"},
+ Removed: []string{"oldfile.txt"},
+ Modified: []string{"modified.txt"},
+ },
+ {
+ ID: "def456",
+ Message: "Another commit",
+ Added: []string{"file3.txt"},
+ Removed: []string{},
+ Modified: []string{"file1.txt"},
+ },
+ }
+ apiHeadCommit := &api.PayloadCommit{
+ ID: "def456",
+ Message: "Another commit",
+ Added: []string{"file3.txt"},
+ Removed: []string{},
+ Modified: []string{"file1.txt"},
+ }
+
+ // Should not modify anything when optimization is disabled
+ optimizedCommits, optimizedHeadCommit := (&webhookNotifier{}).applyWebhookPayloadOptimizations(db.DefaultContext, repo, apiCommits, apiHeadCommit)
+ if assert.NotNil(t, optimizedCommits) && len(optimizedCommits) == 2 {
+ assert.Equal(t, []string{"file1.txt", "file2.txt"}, optimizedCommits[0].Added)
+ assert.Equal(t, []string{"oldfile.txt"}, optimizedCommits[0].Removed)
+ assert.Equal(t, []string{"modified.txt"}, optimizedCommits[0].Modified)
+ assert.Equal(t, []string{"file3.txt"}, optimizedCommits[1].Added)
+ assert.Equal(t, []string{}, optimizedCommits[1].Removed)
+ assert.Equal(t, []string{"file1.txt"}, optimizedCommits[1].Modified)
+ }
+ if assert.NotNil(t, optimizedHeadCommit) {
+ assert.Equal(t, []string{"file3.txt"}, optimizedHeadCommit.Added)
+ assert.Equal(t, []string{}, optimizedHeadCommit.Removed)
+ assert.Equal(t, []string{"file1.txt"}, optimizedHeadCommit.Modified)
+ }
+
+ // Test case 2: Files optimization enabled, limit = 0 (trim all)
+ webhook.SetMetaSettings(&webhook_model.MetaSettings{
+ PayloadOptimization: &webhook_model.PayloadOptimizationConfig{
+ Files: &webhook_model.PayloadOptimizationItem{Enable: true, Limit: 0},
+ Commits: &webhook_model.PayloadOptimizationItem{Enable: false, Limit: 0},
+ },
+ })
+ err = webhook_model.UpdateWebhook(db.DefaultContext, webhook)
+ assert.NoError(t, err)
+
+ apiCommits = []*api.PayloadCommit{
+ {
+ ID: "abc123",
+ Message: "Test commit",
+ Added: []string{"file1.txt", "file2.txt"},
+ Removed: []string{"oldfile.txt"},
+ Modified: []string{"modified.txt"},
+ },
+ {
+ ID: "def456",
+ Message: "Another commit",
+ Added: []string{"file3.txt"},
+ Removed: []string{},
+ Modified: []string{"file1.txt"},
+ },
+ }
+ apiHeadCommit = &api.PayloadCommit{
+ ID: "def456",
+ Message: "Another commit",
+ Added: []string{"file3.txt"},
+ Removed: []string{},
+ Modified: []string{"file1.txt"},
+ }
+
+ optimizedCommits, optimizedHeadCommit = (&webhookNotifier{}).applyWebhookPayloadOptimizations(db.DefaultContext, repo, apiCommits, apiHeadCommit)
+ if assert.NotNil(t, optimizedCommits) && len(optimizedCommits) == 2 {
+ assert.Nil(t, optimizedCommits[0].Added)
+ assert.Nil(t, optimizedCommits[0].Removed)
+ assert.Nil(t, optimizedCommits[0].Modified)
+ assert.Nil(t, optimizedCommits[1].Added)
+ assert.Nil(t, optimizedCommits[1].Removed)
+ assert.Nil(t, optimizedCommits[1].Modified)
+ }
+ if assert.NotNil(t, optimizedHeadCommit) {
+ assert.Nil(t, optimizedHeadCommit.Added)
+ assert.Nil(t, optimizedHeadCommit.Removed)
+ assert.Nil(t, optimizedHeadCommit.Modified)
+ }
+
+ // Test case 3: Commits optimization enabled, limit = 1 (keep first)
+ webhook.SetMetaSettings(&webhook_model.MetaSettings{
+ PayloadOptimization: &webhook_model.PayloadOptimizationConfig{
+ Files: &webhook_model.PayloadOptimizationItem{Enable: false, Limit: 0},
+ Commits: &webhook_model.PayloadOptimizationItem{Enable: true, Limit: 1},
+ },
+ })
+ err = webhook_model.UpdateWebhook(db.DefaultContext, webhook)
+ assert.NoError(t, err)
+
+ apiCommits = []*api.PayloadCommit{
+ {
+ ID: "abc123",
+ Message: "Test commit",
+ Added: []string{"file1.txt", "file2.txt"},
+ Removed: []string{"oldfile.txt"},
+ Modified: []string{"modified.txt"},
+ },
+ {
+ ID: "def456",
+ Message: "Another commit",
+ Added: []string{"file3.txt"},
+ Removed: []string{},
+ Modified: []string{"file1.txt"},
+ },
+ }
+ apiHeadCommit = &api.PayloadCommit{
+ ID: "def456",
+ Message: "Another commit",
+ Added: []string{"file3.txt"},
+ Removed: []string{},
+ Modified: []string{"file1.txt"},
+ }
+
+ optimizedCommits, optimizedHeadCommit = (&webhookNotifier{}).applyWebhookPayloadOptimizations(db.DefaultContext, repo, apiCommits, apiHeadCommit)
+ if assert.NotNil(t, optimizedCommits) && len(optimizedCommits) == 1 {
+ assert.Equal(t, []string{"file1.txt", "file2.txt"}, optimizedCommits[0].Added)
+ assert.Equal(t, []string{"oldfile.txt"}, optimizedCommits[0].Removed)
+ assert.Equal(t, []string{"modified.txt"}, optimizedCommits[0].Modified)
+ }
+ if assert.NotNil(t, optimizedHeadCommit) {
+ assert.Equal(t, []string{"file3.txt"}, optimizedHeadCommit.Added)
+ assert.Equal(t, []string{}, optimizedHeadCommit.Removed)
+ assert.Equal(t, []string{"file1.txt"}, optimizedHeadCommit.Modified)
+ }
+}
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index a8ad1d6c9e5cf..0994df6801512 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -47,6 +47,50 @@
{{ctx.Locale.Tr "repo.settings.branch_filter_desc" "https://pkg.go.dev/github.com/gobwas/glob#Compile" "github.com/gobwas/glob"}}
+
+