From 141eb4ea571947deff756878a5b70dec06373137 Mon Sep 17 00:00:00 2001 From: Michael de Wit Date: Tue, 3 Jan 2017 09:38:05 +0100 Subject: [PATCH] Add pull_request webhook support to Gogs remote --- remote/gogs/fixtures/hooks.go | 52 ++++++++++++++++++++++ remote/gogs/helper.go | 40 +++++++++++++++++ remote/gogs/helper_test.go | 51 ++++++++++++++++++++++ remote/gogs/parse.go | 39 +++++++++++++++-- remote/gogs/types.go | 82 +++++++++++++++++++++++++++++++++++ 5 files changed, 261 insertions(+), 3 deletions(-) diff --git a/remote/gogs/fixtures/hooks.go b/remote/gogs/fixtures/hooks.go index fb04b2b37..0fd5eb19a 100644 --- a/remote/gogs/fixtures/hooks.go +++ b/remote/gogs/fixtures/hooks.go @@ -83,3 +83,55 @@ var HookPushTag = `{ "avatar_url": "https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87" } }` + +// HookPullRequest is a sample pull_request webhook payload +var HookPullRequest = `{ + "action": "opened", + "number": 1, + "pull_request": { + "html_url": "http://gogs.golang.org/gordon/hello-world/pull/1", + "state": "open", + "title": "Update the README with new information", + "body": "please merge", + "user": { + "id": 1, + "username": "gordon", + "full_name": "Gordon the Gopher", + "email": "gordon@golang.org", + "avatar_url": "http://gogs.golang.org///1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87" + }, + "base": { + "label": "master", + "ref": "master", + "sha": "9353195a19e45482665306e466c832c46560532d" + }, + "head": { + "label": "feature/changes", + "ref": "feature/changes", + "sha": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c" + } + }, + "repository": { + "id": 35129377, + "name": "hello-world", + "full_name": "gordon/hello-world", + "owner": { + "id": 1, + "username": "gordon", + "full_name": "Gordon the Gopher", + "email": "gordon@golang.org", + "avatar_url": "https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87" + }, + "private": true, + "html_url": "http://gogs.golang.org/gordon/hello-world", + "clone_url": "https://gogs.golang.org/gordon/hello-world.git", + "default_branch": "master" + }, + "sender": { + "id": 1, + "username": "gordon", + "full_name": "Gordon the Gopher", + "email": "gordon@golang.org", + "avatar_url": "https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87" + } +}` diff --git a/remote/gogs/helper.go b/remote/gogs/helper.go index 157171015..e4f3e1387 100644 --- a/remote/gogs/helper.go +++ b/remote/gogs/helper.go @@ -112,6 +112,30 @@ func buildFromTag(hook *pushHook) *model.Build { } } +// helper function that extracts the Build data from a Gogs pull_request hook +func buildFromPullRequest(hook *pullRequestHook) *model.Build { + avatar := expandAvatar( + hook.Repo.URL, + fixMalformedAvatar(hook.PullRequest.User.Avatar), + ) + build := &model.Build{ + Event: model.EventPull, + Commit: hook.PullRequest.Head.Sha, + Link: hook.PullRequest.URL, + Ref: fmt.Sprintf("refs/pull/%d/head", hook.Number), + Branch: hook.PullRequest.Base.Ref, + Message: hook.PullRequest.Title, + Author: hook.PullRequest.User.Username, + Avatar: avatar, + Title: hook.PullRequest.Title, + Refspec: fmt.Sprintf("%s:%s", + hook.PullRequest.Head.Ref, + hook.PullRequest.Base.Ref, + ), + } + return build +} + // helper function that extracts the Repository data from a Gogs push hook func repoFromPush(hook *pushHook) *model.Repo { return &model.Repo{ @@ -122,6 +146,16 @@ func repoFromPush(hook *pushHook) *model.Repo { } } +// helper function that extracts the Repository data from a Gogs pull_request hook +func repoFromPullRequest(hook *pullRequestHook) *model.Repo { + return &model.Repo{ + Name: hook.Repo.Name, + Owner: hook.Repo.Owner.Username, + FullName: hook.Repo.FullName, + Link: hook.Repo.URL, + } +} + // helper function that parses a push hook from a read closer. func parsePush(r io.Reader) (*pushHook, error) { push := new(pushHook) @@ -129,6 +163,12 @@ func parsePush(r io.Reader) (*pushHook, error) { return push, err } +func parsePullRequest(r io.Reader) (*pullRequestHook, error) { + pr := new(pullRequestHook) + err := json.NewDecoder(r).Decode(pr) + return pr, err +} + // fixMalformedAvatar is a helper function that fixes an avatar url if malformed // (currently a known bug with gogs) func fixMalformedAvatar(url string) string { diff --git a/remote/gogs/helper_test.go b/remote/gogs/helper_test.go index b3f91c6c2..f8ad2f4e0 100644 --- a/remote/gogs/helper_test.go +++ b/remote/gogs/helper_test.go @@ -53,6 +53,32 @@ func Test_parse(t *testing.T) { g.Assert(hook.Sender.Avatar).Equal("https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87") }) + g.It("Should parse pull_request hook payload", func() { + buf := bytes.NewBufferString(fixtures.HookPullRequest) + hook, err := parsePullRequest(buf) + g.Assert(err == nil).IsTrue() + g.Assert(hook.Action).Equal("opened") + g.Assert(hook.Number).Equal(int64(1)) + + g.Assert(hook.Repo.Name).Equal("hello-world") + g.Assert(hook.Repo.URL).Equal("http://gogs.golang.org/gordon/hello-world") + g.Assert(hook.Repo.FullName).Equal("gordon/hello-world") + g.Assert(hook.Repo.Owner.Email).Equal("gordon@golang.org") + g.Assert(hook.Repo.Owner.Username).Equal("gordon") + g.Assert(hook.Repo.Private).Equal(true) + g.Assert(hook.Sender.Username).Equal("gordon") + g.Assert(hook.Sender.Avatar).Equal("https://secure.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87") + + g.Assert(hook.PullRequest.Title).Equal("Update the README with new information") + g.Assert(hook.PullRequest.Body).Equal("please merge") + g.Assert(hook.PullRequest.State).Equal("open") + g.Assert(hook.PullRequest.User.Username).Equal("gordon") + g.Assert(hook.PullRequest.Base.Label).Equal("master") + g.Assert(hook.PullRequest.Base.Ref).Equal("master") + g.Assert(hook.PullRequest.Head.Label).Equal("feature/changes") + g.Assert(hook.PullRequest.Head.Ref).Equal("feature/changes") + }) + g.It("Should return a Build struct from a push hook", func() { buf := bytes.NewBufferString(fixtures.HookPush) hook, _ := parsePush(buf) @@ -78,6 +104,31 @@ func Test_parse(t *testing.T) { g.Assert(repo.Link).Equal(hook.Repo.URL) }) + g.It("Should return a Build struct from a pull_request hook", func() { + buf := bytes.NewBufferString(fixtures.HookPullRequest) + hook, _ := parsePullRequest(buf) + build := buildFromPullRequest(hook) + g.Assert(build.Event).Equal(model.EventPull) + g.Assert(build.Commit).Equal(hook.PullRequest.Head.Sha) + g.Assert(build.Ref).Equal("refs/pull/1/head") + g.Assert(build.Link).Equal(hook.PullRequest.URL) + g.Assert(build.Branch).Equal("master") + g.Assert(build.Message).Equal(hook.PullRequest.Title) + g.Assert(build.Avatar).Equal("http://1.gravatar.com/avatar/8c58a0be77ee441bb8f8595b7f1b4e87") + g.Assert(build.Author).Equal(hook.PullRequest.User.Username) + + }) + + g.It("Should return a Repo struct from a pull_request hook", func() { + buf := bytes.NewBufferString(fixtures.HookPullRequest) + hook, _ := parsePullRequest(buf) + repo := repoFromPullRequest(hook) + g.Assert(repo.Name).Equal(hook.Repo.Name) + g.Assert(repo.Owner).Equal(hook.Repo.Owner.Username) + g.Assert(repo.FullName).Equal("gordon/hello-world") + g.Assert(repo.Link).Equal(hook.Repo.URL) + }) + g.It("Should return a Perm struct from a Gogs Perm", func() { perms := []gogs.Permission{ {true, true, true}, diff --git a/remote/gogs/parse.go b/remote/gogs/parse.go index f1e4f4429..6b3c1b60d 100644 --- a/remote/gogs/parse.go +++ b/remote/gogs/parse.go @@ -8,9 +8,15 @@ import ( ) const ( - hookEvent = "X-Gogs-Event" - hookPush = "push" - hookCreated = "create" + hookEvent = "X-Gogs-Event" + hookPush = "push" + hookCreated = "create" + hookPullRequest = "pull_request" + + actionOpen = "opened" + actionSync = "synchronize" + + stateOpen = "open" refBranch = "branch" refTag = "tag" @@ -24,6 +30,8 @@ func parseHook(r *http.Request) (*model.Repo, *model.Build, error) { return parsePushHook(r.Body) case hookCreated: return parseCreatedHook(r.Body) + case hookPullRequest: + return parsePullRequestHook(r.Body) } return nil, nil, nil } @@ -72,3 +80,28 @@ func parseCreatedHook(payload io.Reader) (*model.Repo, *model.Build, error) { build = buildFromTag(push) return repo, build, err } + +// parsePullRequestHook parses a pull_request hook and returns the Repo and Build details. +func parsePullRequestHook(payload io.Reader) (*model.Repo, *model.Build, error) { + var ( + repo *model.Repo + build *model.Build + ) + + pr, err := parsePullRequest(payload) + if err != nil { + return nil, nil, err + } + + // Don't trigger builds for non-code changes, or if PR is not open + if pr.Action != actionOpen && pr.Action != actionSync { + return nil, nil, nil + } + if pr.PullRequest.State != stateOpen { + return nil, nil, nil + } + + repo = repoFromPullRequest(pr) + build = buildFromPullRequest(pr) + return repo, build, err +} diff --git a/remote/gogs/types.go b/remote/gogs/types.go index 53e21bb5e..0b0949d46 100644 --- a/remote/gogs/types.go +++ b/remote/gogs/types.go @@ -40,3 +40,85 @@ type pushHook struct { Avatar string `json:"avatar_url"` } `json:"sender"` } + +type pullRequestHook struct { + Action string `json:"action"` + Number int64 `json:"number"` + PullRequest struct { + ID int64 `json:"id"` + User struct { + ID int64 `json:"id"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"user"` + Title string `json:"title"` + Body string `json:"body"` + Labels []string `json:"labels"` + State string `json:"state"` + URL string `json:"html_url"` + Mergeable bool `json:"mergeable"` + Merged bool `json:"merged"` + MergeBase string `json:"merge_base"` + Base struct { + Label string `json:"label"` + Ref string `json:"ref"` + Sha string `json:"sha"` + Repo struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + URL string `json:"html_url"` + Private bool `json:"private"` + Owner struct { + ID int64 `json:"id"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"owner"` + } `json:"repo"` + } `json:"base"` + Head struct { + Label string `json:"label"` + Ref string `json:"ref"` + Sha string `json:"sha"` + Repo struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + URL string `json:"html_url"` + Private bool `json:"private"` + Owner struct { + ID int64 `json:"id"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"owner"` + } `json:"repo"` + } `json:"head"` + } `json:"pull_request"` + Repo struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + URL string `json:"html_url"` + Private bool `json:"private"` + Owner struct { + ID int64 `json:"id"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"owner"` + } `json:"repository"` + Sender struct { + ID int64 `json:"id"` + Username string `json:"username"` + Name string `json:"full_name"` + Email string `json:"email"` + Avatar string `json:"avatar_url"` + } `json:"sender"` +}