diff --git a/internal/gitsources/agolagit/parse.go b/internal/gitsources/agolagit/parse.go index 56e657e..a4ec5c9 100644 --- a/internal/gitsources/agolagit/parse.go +++ b/internal/gitsources/agolagit/parse.go @@ -18,14 +18,12 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "path" "strconv" "strings" "github.com/sorintlab/agola/internal/services/types" - "github.com/sorintlab/agola/internal/util" "github.com/pkg/errors" ) @@ -93,7 +91,6 @@ func parsePullRequestHook(payload io.Reader) (*types.WebhookData, error) { } func webhookDataFromPush(hook *pushHook) (*types.WebhookData, error) { - log.Printf("hook: %s", util.Dump(hook)) sender := hook.Sender.Username if sender == "" { sender = hook.Sender.Login @@ -125,7 +122,6 @@ func webhookDataFromPush(hook *pushHook) (*types.WebhookData, error) { // helper function that extracts the Build data from a Gitea pull_request hook func webhookDataFromPullRequest(hook *pullRequestHook) *types.WebhookData { - log.Printf("hook: %s", util.Dump(hook)) sender := hook.Sender.Username if sender == "" { sender = hook.Sender.Login diff --git a/internal/gitsources/gitlab/gitlab.go b/internal/gitsources/gitlab/gitlab.go index b0be5f7..a44e24e 100644 --- a/internal/gitsources/gitlab/gitlab.go +++ b/internal/gitsources/gitlab/gitlab.go @@ -210,6 +210,7 @@ func (c *Client) CreateRepoWebhook(repopath, url, secret string) error { opts := &gitlab.AddProjectHookOptions{ URL: gitlab.String(url), PushEvents: gitlab.Bool(true), + TagPushEvents: gitlab.Bool(true), MergeRequestsEvents: gitlab.Bool(true), } _, _, err := c.client.Projects.AddProjectHook(repopath, opts) @@ -237,5 +238,5 @@ func (c *Client) DeleteRepoWebhook(repopath, u string) error { } func (c *Client) ParseWebhook(r *http.Request) (*types.WebhookData, error) { - return nil, errors.Errorf("unimplemented") + return parseWebhook(r) } diff --git a/internal/gitsources/gitlab/parse.go b/internal/gitsources/gitlab/parse.go new file mode 100644 index 0000000..cd89d91 --- /dev/null +++ b/internal/gitsources/gitlab/parse.go @@ -0,0 +1,171 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package gitlab + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + + "github.com/sorintlab/agola/internal/services/types" + + "github.com/pkg/errors" +) + +const ( + hookEvent = "X-Gitlab-Event" + + hookPush = "Push Hook" + hookTagPush = "Tag Push Hook" + hookPullRequest = "Merge Request Hook" + + prStateOpen = "open" + + prActionOpen = "opened" + prActionSync = "synchronized" +) + +func parseWebhook(r *http.Request) (*types.WebhookData, error) { + switch r.Header.Get(hookEvent) { + case hookPush: + return parsePushHook(r.Body) + case hookTagPush: + return parsePushHook(r.Body) + case hookPullRequest: + return parsePullRequestHook(r.Body) + default: + return nil, errors.Errorf("unknown webhook event type: %q", r.Header.Get(hookEvent)) + } +} + +func parsePush(r io.Reader) (*pushHook, error) { + push := new(pushHook) + err := json.NewDecoder(r).Decode(push) + return push, err +} + +func parsePullRequest(r io.Reader) (*pullRequestHook, error) { + pr := new(pullRequestHook) + err := json.NewDecoder(r).Decode(pr) + return pr, err +} + +func parsePushHook(payload io.Reader) (*types.WebhookData, error) { + push, err := parsePush(payload) + if err != nil { + return nil, err + } + + // skip push events with 0 commits. i.e. a tag deletion. + if len(push.Commits) == 0 { + return nil, nil + } + + return webhookDataFromPush(push) +} + +func parsePullRequestHook(payload io.Reader) (*types.WebhookData, error) { + prhook, err := parsePullRequest(payload) + if err != nil { + return nil, err + } + + // // skip non open pull requests + // if prhook.PullRequest.State != prStateOpen { + // return nil, nil + // } + // // only accept actions that have new commits + // if prhook.Action != prActionOpen && prhook.Action != prActionSync { + // return nil, nil + // } + + return webhookDataFromPullRequest(prhook), nil +} + +func webhookDataFromPush(hook *pushHook) (*types.WebhookData, error) { + sender := hook.UserName + if sender == "" { + sender = hook.UserUsername + } + + // common data + whd := &types.WebhookData{ + CommitSHA: hook.After, + Ref: hook.Ref, + CommitLink: hook.Commits[0].URL, + //CompareLink: hook.Compare, + //CommitLink: fmt.Sprintf("%s/commit/%s", hook.Repo.URL, hook.After), + Sender: sender, + + Repo: types.WebhookDataRepo{ + Path: hook.Project.PathWithNamespace, + WebURL: hook.Project.WebURL, + }, + } + + switch { + case strings.HasPrefix(hook.Ref, "refs/heads/"): + whd.Event = types.WebhookEventPush + whd.Branch = strings.TrimPrefix(hook.Ref, "refs/heads/") + whd.BranchLink = fmt.Sprintf("%s/tree/%s", hook.Project.WebURL, whd.Branch) + if len(hook.Commits) > 0 { + whd.Message = hook.Commits[0].Message + } + case strings.HasPrefix(hook.Ref, "refs/tags/"): + whd.Event = types.WebhookEventTag + whd.Tag = strings.TrimPrefix(hook.Ref, "refs/tags/") + whd.TagLink = fmt.Sprintf("%s/tree/%s", hook.Project.WebURL, whd.Tag) + whd.Message = fmt.Sprintf("Tag %s", whd.Tag) + default: + // ignore received webhook since it doesn't have a ref we're interested in + return nil, fmt.Errorf("unsupported webhook ref %q", hook.Ref) + } + + return whd, nil +} + +// helper function that extracts the Build data from a Gitea pull_request hook +func webhookDataFromPullRequest(hook *pullRequestHook) *types.WebhookData { + // TODO(sgotti) Use PR opener username or last commit user name? + sender := hook.User.Name + if sender == "" { + sender = hook.User.Username + } + //sender := hook.ObjectAttributes.LastCommit.Author.Name + //if sender == "" { + // sender := hook.ObjectAttributes.LastCommit.Author.UserName + //} + build := &types.WebhookData{ + Event: types.WebhookEventPullRequest, + CommitSHA: hook.ObjectAttributes.LastCommit.ID, + Ref: fmt.Sprintf("refs/merge-requests/%d/head", hook.ObjectAttributes.Iid), + //CommitLink: fmt.Sprintf("%s/commit/%s", hook.Repo.URL, hook.PullRequest.Head.Sha), + CommitLink: hook.ObjectAttributes.LastCommit.URL, + Branch: hook.ObjectAttributes.SourceBranch, + Message: hook.ObjectAttributes.Title, + Sender: sender, + PullRequestID: strconv.Itoa(hook.ObjectAttributes.Iid), + PullRequestLink: hook.ObjectAttributes.URL, + + Repo: types.WebhookDataRepo{ + Path: hook.Project.PathWithNamespace, + WebURL: hook.Project.WebURL, + }, + } + return build +} diff --git a/internal/gitsources/gitlab/types.go b/internal/gitsources/gitlab/types.go new file mode 100644 index 0000000..0b05cfc --- /dev/null +++ b/internal/gitsources/gitlab/types.go @@ -0,0 +1,164 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package gitlab + +// TODO(sgotti) generated with https://github.com/mholt/json-to-go and manually +// cleaned. Many fields are not needed, remove them. + +type pushHook struct { + ObjectKind string `json:"object_kind"` + EventName string `json:"event_name"` + Before string `json:"before"` + After string `json:"after"` + Ref string `json:"ref"` + CheckoutSha string `json:"checkout_sha"` + Message string `json:"message"` + UserID int `json:"user_id"` + UserName string `json:"user_name"` + UserUsername string `json:"user_username"` + UserEmail string `json:"user_email"` + UserAvatar string `json:"user_avatar"` + Project struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"project"` + Commits []struct { + ID string `json:"id"` + Message string `json:"message"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + Modified []string `json:"modified"` + } `json:"commits"` + TotalCommitsCount int `json:"total_commits_count"` + Repository struct { + Name string `json:"name"` + URL string `json:"url"` + Description string `json:"description"` + Homepage string `json:"homepage"` + GitHTTPURL string `json:"git_http_url"` + GitSSHURL string `json:"git_ssh_url"` + VisibilityLevel int `json:"visibility_level"` + } `json:"repository"` +} + +type pullRequestHook struct { + ObjectKind string `json:"object_kind"` + EventType string `json:"event_type"` + User struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"user"` + Project struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"project"` + ObjectAttributes struct { + AuthorID int `json:"author_id"` + Description string `json:"description"` + HeadPipelineID int `json:"head_pipeline_id"` + ID int `json:"id"` + Iid int `json:"iid"` + MergeStatus string `json:"merge_status"` + MergeWhenPipelineSucceeds bool `json:"merge_when_pipeline_succeeds"` + SourceBranch string `json:"source_branch"` + SourceProjectID int `json:"source_project_id"` + State string `json:"state"` + TargetBranch string `json:"target_branch"` + TargetProjectID int `json:"target_project_id"` + Title string `json:"title"` + URL string `json:"url"` + Source struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"source"` + Target struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"target"` + LastCommit struct { + ID string `json:"id"` + Message string `json:"message"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + } `json:"last_commit"` + WorkInProgress bool `json:"work_in_progress"` + TotalTimeSpent int `json:"total_time_spent"` + } `json:"object_attributes"` + Changes struct { + TotalTimeSpent struct { + Current int `json:"current"` + } `json:"total_time_spent"` + } `json:"changes"` + Repository struct { + Name string `json:"name"` + URL string `json:"url"` + Description string `json:"description"` + Homepage string `json:"homepage"` + } `json:"repository"` +}