Skip to content

Commit 2d824ab

Browse files
committed
Implement webhook branch filter
See #2025, #3998.
1 parent 487533f commit 2d824ab

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+3162
-10
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ require (
4545
github.com/go-redis/redis v6.15.2+incompatible
4646
github.com/go-sql-driver/mysql v1.4.1
4747
github.com/go-xorm/xorm v0.7.7-0.20190822154023-17592d96b35b
48+
github.com/gobwas/glob v0.2.3
4849
github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561
4950
github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14
5051
github.com/golang/snappy v0.0.1 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuq
150150
github.com/go-xorm/xorm v0.7.6/go.mod h1:nqz2TAsuOHWH2yk4FYWtacCGgdbrcdZ5mF1XadqEHls=
151151
github.com/go-xorm/xorm v0.7.7-0.20190822154023-17592d96b35b h1:Y0hWUheXDHpIs7BWtJcykO4d1VOsVDKg1PsP5YJwxxM=
152152
github.com/go-xorm/xorm v0.7.7-0.20190822154023-17592d96b35b/go.mod h1:nqz2TAsuOHWH2yk4FYWtacCGgdbrcdZ5mF1XadqEHls=
153+
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
154+
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
153155
github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561 h1:deE7ritpK04PgtpyVOS2TYcQEld9qLCD5b5EbVNOuLA=
154156
github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:YgYOrVn3Nj9Tq0EvjmFbphRytDj7JNRoWSStJZWDJTQ=
155157
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=

models/fixtures/webhook.yml

+7
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,10 @@
2222
content_type: 1 # json
2323
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
2424
is_active: true
25+
-
26+
id: 4
27+
repo_id: 2
28+
url: www.example.com/url4
29+
content_type: 1 # json
30+
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
31+
is_active: true

models/webhook.go

+49-3
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ import (
1919
"strings"
2020
"time"
2121

22+
"code.gitea.io/gitea/modules/git"
2223
"code.gitea.io/gitea/modules/log"
2324
"code.gitea.io/gitea/modules/setting"
2425
api "code.gitea.io/gitea/modules/structs"
2526
"code.gitea.io/gitea/modules/sync"
2627
"code.gitea.io/gitea/modules/timeutil"
2728

29+
"github.com/gobwas/glob"
2830
gouuid "github.com/satori/go.uuid"
2931
"github.com/unknwon/com"
3032
)
@@ -84,9 +86,10 @@ type HookEvents struct {
8486

8587
// HookEvent represents events that will delivery hook.
8688
type HookEvent struct {
87-
PushOnly bool `json:"push_only"`
88-
SendEverything bool `json:"send_everything"`
89-
ChooseEvents bool `json:"choose_events"`
89+
PushOnly bool `json:"push_only"`
90+
SendEverything bool `json:"send_everything"`
91+
ChooseEvents bool `json:"choose_events"`
92+
BranchFilter string `json:"branch_filter"`
9093

9194
HookEvents `json:"events"`
9295
}
@@ -256,6 +259,21 @@ func (w *Webhook) EventsArray() []string {
256259
return events
257260
}
258261

262+
func (w *Webhook) checkBranch(branch string) bool {
263+
if w.BranchFilter == "" || w.BranchFilter == "*" {
264+
return true
265+
}
266+
267+
g, err := glob.Compile(w.BranchFilter)
268+
if err != nil {
269+
// should not really happen as BranchFilter is validated
270+
log.Error("CheckBranch failed: %s", err)
271+
return false
272+
}
273+
274+
return g.Match(branch)
275+
}
276+
259277
// CreateWebhook creates a new web hook.
260278
func CreateWebhook(w *Webhook) error {
261279
return createWebhook(x, w)
@@ -651,6 +669,25 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay
651669
return prepareWebhook(x, w, repo, event, p)
652670
}
653671

672+
// getPayloadBranch returns branch for hook event, if applicable.
673+
func getPayloadBranch(p api.Payloader) string {
674+
switch pp := p.(type) {
675+
case *api.CreatePayload:
676+
if pp.RefType == "branch" {
677+
return pp.Ref
678+
}
679+
case *api.DeletePayload:
680+
if pp.RefType == "branch" {
681+
return pp.Ref
682+
}
683+
case *api.PushPayload:
684+
if strings.HasPrefix(pp.Ref, git.BranchPrefix) {
685+
return pp.Ref[len(git.BranchPrefix):]
686+
}
687+
}
688+
return ""
689+
}
690+
654691
func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error {
655692
for _, e := range w.eventCheckers() {
656693
if event == e.typ {
@@ -660,6 +697,15 @@ func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType,
660697
}
661698
}
662699

700+
// If payload has no associated branch (e.g. it's a new tag, issue, etc.),
701+
// branch filter has no effect.
702+
if branch := getPayloadBranch(p); branch != "" {
703+
if !w.checkBranch(branch) {
704+
log.Info("Branch %q doesn't match branch filter %q, skipping", branch, w.BranchFilter)
705+
return nil
706+
}
707+
}
708+
663709
var payloader api.Payloader
664710
var err error
665711
// Use separate objects so modifications won't be made on payload on non-Gogs/Gitea type hooks.

models/webhook_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,40 @@ func TestPrepareWebhooks(t *testing.T) {
270270
}
271271
}
272272

273+
func TestPrepareWebhooksBranchFilterMatch(t *testing.T) {
274+
assert.NoError(t, PrepareTestDatabase())
275+
276+
repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
277+
hookTasks := []*HookTask{
278+
{RepoID: repo.ID, HookID: 4, EventType: HookEventPush},
279+
}
280+
for _, hookTask := range hookTasks {
281+
AssertNotExistsBean(t, hookTask)
282+
}
283+
// this test also ensures that * doesn't handle / in any special way (like shell would)
284+
assert.NoError(t, PrepareWebhooks(repo, HookEventPush, &api.PushPayload{Ref: "refs/heads/feature/7791"}))
285+
for _, hookTask := range hookTasks {
286+
AssertExistsAndLoadBean(t, hookTask)
287+
}
288+
}
289+
290+
func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
291+
assert.NoError(t, PrepareTestDatabase())
292+
293+
repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
294+
hookTasks := []*HookTask{
295+
{RepoID: repo.ID, HookID: 4, EventType: HookEventPush},
296+
}
297+
for _, hookTask := range hookTasks {
298+
AssertNotExistsBean(t, hookTask)
299+
}
300+
assert.NoError(t, PrepareWebhooks(repo, HookEventPush, &api.PushPayload{Ref: "refs/heads/fix_weird_bug"}))
301+
302+
for _, hookTask := range hookTasks {
303+
AssertNotExistsBean(t, hookTask)
304+
}
305+
}
306+
273307
// TODO TestHookTask_deliver
274308

275309
// TODO TestDeliverHooks

modules/auth/auth.go

+2
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro
357357
data["ErrorMsg"] = trName + l.Tr("form.url_error")
358358
case binding.ERR_INCLUDE:
359359
data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field))
360+
case validation.ErrGlobPattern:
361+
data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message)
360362
default:
361363
data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification
362364
}

modules/auth/repo_form.go

+1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ type WebhookForm struct {
184184
PullRequest bool
185185
Repository bool
186186
Active bool
187+
BranchFilter string `binding:"GlobPattern"`
187188
}
188189

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

modules/structs/hook.go

+7-5
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,19 @@ type CreateHookOption struct {
4040
// enum: gitea,gogs,slack,discord
4141
Type string `json:"type" binding:"Required"`
4242
// required: true
43-
Config map[string]string `json:"config" binding:"Required"`
44-
Events []string `json:"events"`
43+
Config map[string]string `json:"config" binding:"Required"`
44+
Events []string `json:"events"`
45+
BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
4546
// default: false
4647
Active bool `json:"active"`
4748
}
4849

4950
// EditHookOption options when modify one hook
5051
type EditHookOption struct {
51-
Config map[string]string `json:"config"`
52-
Events []string `json:"events"`
53-
Active *bool `json:"active"`
52+
Config map[string]string `json:"config"`
53+
Events []string `json:"events"`
54+
BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
55+
Active *bool `json:"active"`
5456
}
5557

5658
// Payloader payload is some part of one hook

modules/validation/binding.go

+25
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ import (
1010
"strings"
1111

1212
"gitea.com/macaron/binding"
13+
"github.com/gobwas/glob"
1314
)
1415

1516
const (
1617
// ErrGitRefName is git reference name error
1718
ErrGitRefName = "GitRefNameError"
19+
20+
// ErrGlobPattern is returned when glob pattern is invalid
21+
ErrGlobPattern = "GlobPattern"
1822
)
1923

2024
var (
@@ -28,6 +32,7 @@ var (
2832
func AddBindingRules() {
2933
addGitRefNameBindingRule()
3034
addValidURLBindingRule()
35+
addGlobPatternRule()
3136
}
3237

3338
func addGitRefNameBindingRule() {
@@ -82,6 +87,26 @@ func addValidURLBindingRule() {
8287
})
8388
}
8489

90+
func addGlobPatternRule() {
91+
binding.AddRule(&binding.Rule{
92+
IsMatch: func(rule string) bool {
93+
return rule == "GlobPattern"
94+
},
95+
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
96+
str := fmt.Sprintf("%v", val)
97+
98+
if len(str) != 0 {
99+
if _, err := glob.Compile(str); err != nil {
100+
errs.Add([]string{name}, ErrGlobPattern, err.Error())
101+
return false, errs
102+
}
103+
}
104+
105+
return true, errs
106+
},
107+
})
108+
}
109+
85110
func portOnly(hostport string) string {
86111
colon := strings.IndexByte(hostport, ':')
87112
if colon == -1 {

modules/validation/binding_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ type (
2626
}
2727

2828
TestForm struct {
29-
BranchName string `form:"BranchName" binding:"GitRefName"`
30-
URL string `form:"ValidUrl" binding:"ValidUrl"`
29+
BranchName string `form:"BranchName" binding:"GitRefName"`
30+
URL string `form:"ValidUrl" binding:"ValidUrl"`
31+
GlobPattern string `form:"GlobPattern" binding:"GlobPattern"`
3132
}
3233
)
3334

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package validation
6+
7+
import (
8+
"testing"
9+
10+
"gitea.com/macaron/binding"
11+
"github.com/gobwas/glob"
12+
)
13+
14+
func getGlobPatternErrorString(pattern string) string {
15+
// It would be unwise to rely on that glob
16+
// compilation errors don't ever change.
17+
if _, err := glob.Compile(pattern); err != nil {
18+
return err.Error()
19+
}
20+
return ""
21+
}
22+
23+
var globValidationTestCases = []validationTestCase{
24+
{
25+
description: "Empty glob pattern",
26+
data: TestForm{
27+
GlobPattern: "",
28+
},
29+
expectedErrors: binding.Errors{},
30+
},
31+
{
32+
description: "Valid glob",
33+
data: TestForm{
34+
GlobPattern: "{master,release*}",
35+
},
36+
expectedErrors: binding.Errors{},
37+
},
38+
39+
{
40+
description: "Invalid glob",
41+
data: TestForm{
42+
GlobPattern: "[a-",
43+
},
44+
expectedErrors: binding.Errors{
45+
binding.Error{
46+
FieldNames: []string{"GlobPattern"},
47+
Classification: ErrGlobPattern,
48+
Message: getGlobPatternErrorString("[a-"),
49+
},
50+
},
51+
},
52+
}
53+
54+
func Test_GlobPatternValidation(t *testing.T) {
55+
AddBindingRules()
56+
57+
for _, testCase := range globValidationTestCases {
58+
t.Run(testCase.description, func(t *testing.T) {
59+
performValidationTest(t, testCase)
60+
})
61+
}
62+
}

options/locale/locale_en-US.ini

+3
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ max_size_error = ` must contain at most %s characters.`
300300
email_error = ` is not a valid email address.`
301301
url_error = ` is not a valid URL.`
302302
include_error = ` must contain substring '%s'.`
303+
glob_pattern_error = ` glob pattern is invalid: %s.`
303304
unknown_error = Unknown error:
304305
captcha_incorrect = The CAPTCHA code is incorrect.
305306
password_not_match = The passwords do not match.
@@ -1256,6 +1257,8 @@ settings.event_pull_request = Pull Request
12561257
settings.event_pull_request_desc = Pull request opened, closed, reopened, edited, approved, rejected, review comment, assigned, unassigned, label updated, label cleared or synchronized.
12571258
settings.event_push = Push
12581259
settings.event_push_desc = Git push to a repository.
1260+
settings.branch_filter = Branch filter
1261+
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="https://godoc.org/github.com/gobwas/glob#Compile">github.com/gobwas/glob</a> documentation for syntax. Examples: <code>master</code>, <code>{master,release*}</code>.
12591262
settings.event_repository = Repository
12601263
settings.event_repository_desc = Repository created or deleted.
12611264
settings.active = Active

routers/api/v1/utils/hook.go

+2
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID
112112
Repository: com.IsSliceContainsStr(form.Events, string(models.HookEventRepository)),
113113
Release: com.IsSliceContainsStr(form.Events, string(models.HookEventRelease)),
114114
},
115+
BranchFilter: form.BranchFilter,
115116
},
116117
IsActive: form.Active,
117118
HookTaskType: models.ToHookTaskType(form.Type),
@@ -236,6 +237,7 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *models.Webho
236237
w.PullRequest = com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest))
237238
w.Repository = com.IsSliceContainsStr(form.Events, string(models.HookEventRepository))
238239
w.Release = com.IsSliceContainsStr(form.Events, string(models.HookEventRelease))
240+
w.BranchFilter = form.BranchFilter
239241

240242
if err := w.UpdateEvent(); err != nil {
241243
ctx.Error(500, "UpdateEvent", err)

routers/repo/webhook.go

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ func ParseHookEvent(form auth.WebhookForm) *models.HookEvent {
145145
PullRequest: form.PullRequest,
146146
Repository: form.Repository,
147147
},
148+
BranchFilter: form.BranchFilter,
148149
}
149150
}
150151

templates/repo/settings/webhook/settings.tmpl

+7
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@
116116
</div>
117117
</div>
118118

119+
<!-- Branch filter -->
120+
<div class="field">
121+
<label for="branch_filter">{{.i18n.Tr "repo.settings.branch_filter"}}</label>
122+
<input name="branch_filter" type="text" tabindex="0" value="{{or .Webhook.BranchFilter "*"}}">
123+
<span class="help">{{.i18n.Tr "repo.settings.branch_filter_desc" | Str2html}}</span>
124+
</div>
125+
119126
<div class="ui divider"></div>
120127

121128
<div class="inline field">

0 commit comments

Comments
 (0)