Skip to content

Commit

Permalink
Implement webhook branch filter
Browse files Browse the repository at this point in the history
See #2025, #3998.
  • Loading branch information
WGH- committed Aug 8, 2019
1 parent aead030 commit 7cd5748
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 8 deletions.
7 changes: 7 additions & 0 deletions models/fixtures/webhook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@
content_type: 1 # json
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
is_active: true
-
id: 4
repo_id: 2
url: www.example.com/url4
content_type: 1 # json
events: '{"push_only":true,"branch_filter":"(master|release)"}'
is_active: true
51 changes: 48 additions & 3 deletions models/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import (
"net"
"net/http"
"net/url"
"regexp"
"strings"
"time"

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
Expand Down Expand Up @@ -83,9 +85,10 @@ type HookEvents struct {

// HookEvent represents events that will delivery hook.
type HookEvent struct {
PushOnly bool `json:"push_only"`
SendEverything bool `json:"send_everything"`
ChooseEvents bool `json:"choose_events"`
PushOnly bool `json:"push_only"`
SendEverything bool `json:"send_everything"`
ChooseEvents bool `json:"choose_events"`
BranchFilter string `json:"branch_filter"`

HookEvents `json:"events"`
}
Expand Down Expand Up @@ -255,6 +258,20 @@ func (w *Webhook) EventsArray() []string {
return events
}

func (w *Webhook) checkBranch(branch string) bool {
if w.BranchFilter == "" {
return true
}
pattern := "^" + w.BranchFilter + "$"
matched, err := regexp.MatchString(pattern, branch)
if err != nil {
// should not really happen as BranchFilter is validated
log.Error("CheckBranch failed: %s", err)
return false
}
return matched
}

// CreateWebhook creates a new web hook.
func CreateWebhook(w *Webhook) error {
return createWebhook(x, w)
Expand Down Expand Up @@ -650,6 +667,25 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay
return prepareWebhook(x, w, repo, event, p)
}

// getPayloadBranch returns branch for hook event, if applicable.
func getPayloadBranch(p api.Payloader) (string, bool) {
switch pp := p.(type) {
case *api.CreatePayload:
if pp.RefType == "branch" {
return pp.Ref, true
}
case *api.DeletePayload:
if pp.RefType == "branch" {
return pp.Ref, true
}
case *api.PushPayload:
if strings.HasPrefix(pp.Ref, git.BranchPrefix) {
return pp.Ref[len(git.BranchPrefix):], true
}
}
return "", false
}

func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error {
for _, e := range w.eventCheckers() {
if event == e.typ {
Expand All @@ -659,6 +695,15 @@ func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType,
}
}

// If payload has no associated branch (e.g. it's a new tag, issue, etc.),
// branch filter has no effect.
if branch, ok := getPayloadBranch(p); ok {
if !w.checkBranch(branch) {
log.Info("Branch %q doesn't match branch filter %q, skipping", branch, w.BranchFilter)
return nil
}
}

var payloader api.Payloader
var err error
// Use separate objects so modifications won't be made on payload on non-Gogs/Gitea type hooks.
Expand Down
33 changes: 33 additions & 0 deletions models/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,39 @@ func TestPrepareWebhooks(t *testing.T) {
}
}

func TestPrepareWebhooksBranchFilterMatch(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())

repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
hookTasks := []*HookTask{
{RepoID: repo.ID, HookID: 4, EventType: HookEventPush},
}
for _, hookTask := range hookTasks {
AssertNotExistsBean(t, hookTask)
}
assert.NoError(t, PrepareWebhooks(repo, HookEventPush, &api.PushPayload{Ref: "refs/heads/master"}))
for _, hookTask := range hookTasks {
AssertExistsAndLoadBean(t, hookTask)
}
}

func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())

repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
hookTasks := []*HookTask{
{RepoID: repo.ID, HookID: 4, EventType: HookEventPush},
}
for _, hookTask := range hookTasks {
AssertNotExistsBean(t, hookTask)
}
assert.NoError(t, PrepareWebhooks(repo, HookEventPush, &api.PushPayload{Ref: "refs/heads/fix_weird_bug"}))

for _, hookTask := range hookTasks {
AssertNotExistsBean(t, hookTask)
}
}

// TODO TestHookTask_deliver

// TODO TestDeliverHooks
2 changes: 2 additions & 0 deletions modules/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro
data["ErrorMsg"] = trName + l.Tr("form.url_error")
case binding.ERR_INCLUDE:
data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field))
case validation.ErrRegexp:
data["ErrorMsg"] = trName + l.Tr("form.regexp_error", errs[0].Message)
default:
data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification
}
Expand Down
1 change: 1 addition & 0 deletions modules/auth/repo_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ type WebhookForm struct {
PullRequest bool
Repository bool
Active bool
BranchFilter string `binding:"Regexp"`
}

// PushOnly if the hook will be triggered when push
Expand Down
12 changes: 7 additions & 5 deletions modules/structs/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,19 @@ type CreateHookOption struct {
// enum: gitea,gogs,slack,discord
Type string `json:"type" binding:"Required"`
// required: true
Config map[string]string `json:"config" binding:"Required"`
Events []string `json:"events"`
Config map[string]string `json:"config" binding:"Required"`
Events []string `json:"events"`
BranchFilter string `json:"branch_filter" binding:"Regexp"`
// default: false
Active bool `json:"active"`
}

// EditHookOption options when modify one hook
type EditHookOption struct {
Config map[string]string `json:"config"`
Events []string `json:"events"`
Active *bool `json:"active"`
Config map[string]string `json:"config"`
Events []string `json:"events"`
BranchFilter string `json:"branch_filter" binding:"Regexp"`
Active *bool `json:"active"`
}

// Payloader payload is some part of one hook
Expand Down
24 changes: 24 additions & 0 deletions modules/validation/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import (
const (
// ErrGitRefName is git reference name error
ErrGitRefName = "GitRefNameError"

// ErrRegexp is returned when regexp is invalid
ErrRegexp = "RegexpErr"
)

var (
Expand All @@ -28,6 +31,7 @@ var (
func AddBindingRules() {
addGitRefNameBindingRule()
addValidURLBindingRule()
addRegexpRule()
}

func addGitRefNameBindingRule() {
Expand Down Expand Up @@ -82,6 +86,26 @@ func addValidURLBindingRule() {
})
}

func addRegexpRule() {
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
return strings.HasPrefix(rule, "Regexp")
},
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
str := fmt.Sprintf("%v", val)

if str != "" {
if _, err := regexp.Compile(str); err != nil {
errs.Add([]string{name}, ErrRegexp, err.Error())
return false, errs
}
}

return true, errs
},
})
}

func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
Expand Down
1 change: 1 addition & 0 deletions modules/validation/binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type (
TestForm struct {
BranchName string `form:"BranchName" binding:"GitRefName"`
URL string `form:"ValidUrl" binding:"ValidUrl"`
Regexp string `form:"Regexp" binding:"Regexp"`
}
)

Expand Down
59 changes: 59 additions & 0 deletions modules/validation/regexp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package validation

import (
"regexp"
"testing"

"github.com/go-macaron/binding"
)

func getRegexpErrorString(pattern string) string {
// It would be unwise to rely on that regexp
// compilation errors don't ever change across Go releases.
_, err := regexp.Compile(pattern)
if err != nil {
return err.Error()
}
return ""
}

var regexpValidationTestCases = []validationTestCase{
{
description: "Empty regexp",
data: TestForm{
Regexp: "",
},
expectedErrors: binding.Errors{},
},
{
description: "Valid regexp",
data: TestForm{
Regexp: "(master|release)",
},
expectedErrors: binding.Errors{},
},

{
description: "Invalid regexp",
data: TestForm{
Regexp: "master)(",
},
expectedErrors: binding.Errors{
binding.Error{
FieldNames: []string{"Regexp"},
Classification: ErrRegexp,
Message: getRegexpErrorString("master)("),
},
},
},
}

func Test_RegexpValidation(t *testing.T) {
AddBindingRules()

for _, testCase := range regexpValidationTestCases {
t.Run(testCase.description, func(t *testing.T) {
performValidationTest(t, testCase)
})
}
}
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ max_size_error = ` must contain at most %s characters.`
email_error = ` is not a valid email address.`
url_error = ` is not a valid URL.`
include_error = ` must contain substring '%s'.`
regexp_error = ` regular expression is invalid: %s.`
unknown_error = Unknown error:
captcha_incorrect = The CAPTCHA code is incorrect.
password_not_match = The passwords do not match.
Expand Down Expand Up @@ -1245,6 +1246,8 @@ settings.event_pull_request = Pull Request
settings.event_pull_request_desc = Pull request opened, closed, reopened, edited, approved, rejected, review comment, assigned, unassigned, label updated, label cleared or synchronized.
settings.event_push = Push
settings.event_push_desc = Git push to a repository.
settings.branch_filter = Branch filter
settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as regular expression. If empty, events for all branches are reported. Regular expression is implicitly anchored, that is, partial match is not allowed. Add <code>.*</code> explicitly, if necessary.
settings.event_repository = Repository
settings.event_repository_desc = Repository created or deleted.
settings.active = Active
Expand Down
2 changes: 2 additions & 0 deletions routers/api/v1/utils/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID
Repository: com.IsSliceContainsStr(form.Events, string(models.HookEventRepository)),
Release: com.IsSliceContainsStr(form.Events, string(models.HookEventRelease)),
},
BranchFilter: form.BranchFilter,
},
IsActive: form.Active,
HookTaskType: models.ToHookTaskType(form.Type),
Expand Down Expand Up @@ -236,6 +237,7 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *models.Webho
w.PullRequest = com.IsSliceContainsStr(form.Events, string(models.HookEventPullRequest))
w.Repository = com.IsSliceContainsStr(form.Events, string(models.HookEventRepository))
w.Release = com.IsSliceContainsStr(form.Events, string(models.HookEventRelease))
w.BranchFilter = form.BranchFilter

if err := w.UpdateEvent(); err != nil {
ctx.Error(500, "UpdateEvent", err)
Expand Down
1 change: 1 addition & 0 deletions routers/repo/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func ParseHookEvent(form auth.WebhookForm) *models.HookEvent {
PullRequest: form.PullRequest,
Repository: form.Repository,
},
BranchFilter: form.BranchFilter,
}
}

Expand Down
7 changes: 7 additions & 0 deletions templates/repo/settings/webhook/settings.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@
</div>
</div>

<!-- Branch filter -->
<div class="field">
<label for="branch_filter">{{.i18n.Tr "repo.settings.branch_filter"}}</label>
<input name="branch_filter" type="text" tabindex="0" value="{{.Webhook.BranchFilter}}">
<span class="help">{{.i18n.Tr "repo.settings.branch_filter_desc" | Str2html}}</span>
</div>

<div class="ui divider"></div>

<div class="inline field">
Expand Down
8 changes: 8 additions & 0 deletions templates/swagger/v1_json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -7277,6 +7277,10 @@
"default": false,
"x-go-name": "Active"
},
"branch_filter": {
"type": "string",
"x-go-name": "BranchFilter"
},
"config": {
"type": "object",
"additionalProperties": {
Expand Down Expand Up @@ -7868,6 +7872,10 @@
"type": "boolean",
"x-go-name": "Active"
},
"branch_filter": {
"type": "string",
"x-go-name": "BranchFilter"
},
"config": {
"type": "object",
"additionalProperties": {
Expand Down

0 comments on commit 7cd5748

Please sign in to comment.