You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
go-scm/scm/driver/stash/webhook.go

270 lines
6.9 KiB
Go

// Copyright 2017 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package stash
import (
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"time"
"git.awesome-for.me/liuzhiguo/go-scm/scm"
"git.awesome-for.me/liuzhiguo/go-scm/scm/driver/internal/hmac"
)
// TODO(bradrydzewski) push hook does not include commit message
// TODO(bradrydzewski) push hook does not include commit link
// TODO(bradrydzewski) push hook does not include repository git+http link
// TODO(bradrydzewski) push hook does not include repository git+ssh link
// TODO(bradrydzewski) push hook does not include repository html link
// TODO(bradrydzewski) pr hook does not include repository git+http link
// TODO(bradrydzewski) pr hook does not include repository git+ssh link
// TODO(bradrydzewski) pr hook does not include repository html link
type webhookService struct {
client *wrapper
}
func (s *webhookService) Parse(req *http.Request, fn scm.SecretFunc) (scm.Webhook, error) {
data, err := ioutil.ReadAll(
io.LimitReader(req.Body, 10000000),
)
if err != nil {
return nil, err
}
var hook scm.Webhook
switch req.Header.Get("X-Event-Key") {
case "repo:refs_changed":
hook, err = s.parsePushHook(data)
case "pr:opened", "pr:from_ref_updated", "pr:modified", "pr:declined", "pr:deleted", "pr:merged":
hook, err = s.parsePullRequest(data)
}
if err != nil {
return nil, err
}
if hook == nil {
return nil, nil
}
// get the gogs signature key to verify the payload
// signature. If no key is provided, no validation
// is performed.
key, err := fn(hook)
if err != nil {
return hook, err
} else if key == "" {
return hook, nil
}
sig := req.Header.Get("X-Hub-Signature")
if !hmac.ValidatePrefix(data, []byte(key), sig) {
return hook, scm.ErrSignatureInvalid
}
return hook, nil
}
func (s *webhookService) parsePushHook(data []byte) (scm.Webhook, error) {
dst := new(pushHook)
err := json.Unmarshal(data, dst)
if err != nil {
return nil, err
}
if len(dst.Changes) == 0 {
return nil, errors.New("Push hook has empty changeset")
}
change := dst.Changes[0]
switch {
case change.Ref.Type == "BRANCH" && change.Type != "UPDATE":
return convertBranchHook(dst), nil
case change.Ref.Type == "TAG":
return convertTagHook(dst), nil
default:
return convertPushHook(dst), err
}
}
func (s *webhookService) parsePullRequest(data []byte) (scm.Webhook, error) {
src := new(pullRequestHook)
err := json.Unmarshal(data, src)
if err != nil {
return nil, err
}
dst := convertPullRequestHook(src)
switch src.EventKey {
case "pr:opened":
dst.Action = scm.ActionOpen
case "pr:from_ref_updated":
// The request includes field "previousFromHash", which could be compared
// to the FromRef.Latestcommit to ensure there is actually a change,
// but there is unlikely need for that.
dst.Action = scm.ActionSync
case "pr:modified":
// BitBucket Server (Stash) sends "pr:modified" for any edits to the PR,
// including edits to the title or description. Thus, return the hook
// action only when the target reference has changed (name or hash).
if src.PullRequest.ToRef.DisplayID == src.PreviousTarget.DisplayID &&
src.PullRequest.ToRef.LatestCommit == src.PreviousTarget.LatestCommit {
dst.Action = scm.ActionUpdate
} else {
dst.Action = scm.ActionSync
}
case "pr:declined":
dst.Action = scm.ActionClose
case "pr:deleted":
dst.Action = scm.ActionClose
case "pr:merged":
dst.Action = scm.ActionMerge
default:
return nil, nil
}
return dst, nil
}
//
// native data structures
//
type pushHook struct {
EventKey string `json:"eventKey"`
Date string `json:"date"`
Actor *user `json:"actor"`
Repository *repository `json:"repository"`
Changes []*change `json:"changes"`
}
type pullRequestHook struct {
EventKey string `json:"eventKey"`
Date string `json:"date"`
Actor *user `json:"actor"`
PullRequest *pr `json:"pullRequest"`
// only in pr:from_ref_updated
PreviousFromHash string `json:"previousFromHash"`
// only in pr:modified
PreviousTarget struct {
ID string `json:"id"` // "refs/heads/master"
DisplayID string `json:"displayId"` // "master"
Type string `json:"type"` // "BRANCH"
LatestCommit string `json:"latestCommit"` // "860c4eb4ed0f969b47144234ba13c31c498cca69"
LatestChangeset string `json:"latestChangeset"` // "860c4eb4ed0f969b47144234ba13c31c498cca69"
} `json:"previousTarget"`
}
type change struct {
Ref struct {
ID string `json:"id"`
DisplayID string `json:"displayId"`
Type string `json:"type"`
} `json:"ref"`
RefID string `json:"refId"`
FromHash string `json:"fromHash"`
ToHash string `json:"toHash"`
Type string `json:"type"`
}
//
// push hooks
//
func convertPushHook(src *pushHook) *scm.PushHook {
change := src.Changes[0]
repo := convertRepository(src.Repository)
sender := convertUser(src.Actor)
signer := convertSignature(src.Actor)
signer.Date, _ = time.Parse("2006-01-02T15:04:05+0000", src.Date)
var commits []scm.Commit
for _, c := range src.Changes {
commits = append(commits,
scm.Commit{
Sha: c.ToHash,
Message: "",
Link: "",
})
}
return &scm.PushHook{
Ref: change.RefID,
After: change.ToHash,
Before: change.FromHash,
Commit: scm.Commit{
Sha: change.ToHash,
Message: "",
Link: "",
Author: signer,
Committer: signer,
},
Repo: *repo,
Sender: *sender,
Commits: commits,
}
}
func convertTagHook(src *pushHook) *scm.TagHook {
change := src.Changes[0]
sender := convertUser(src.Actor)
repo := convertRepository(src.Repository)
dst := &scm.TagHook{
Ref: scm.Reference{
Name: change.Ref.DisplayID,
Sha: change.ToHash,
},
Action: scm.ActionCreate,
Repo: *repo,
Sender: *sender,
}
if change.Type == "DELETE" {
dst.Action = scm.ActionDelete
dst.Ref.Sha = change.FromHash
}
return dst
}
func convertBranchHook(src *pushHook) *scm.BranchHook {
change := src.Changes[0]
sender := convertUser(src.Actor)
repo := convertRepository(src.Repository)
dst := &scm.BranchHook{
Ref: scm.Reference{
Name: change.Ref.DisplayID,
Sha: change.ToHash,
},
Action: scm.ActionCreate,
Repo: *repo,
Sender: *sender,
}
if change.Type == "DELETE" {
dst.Action = scm.ActionDelete
dst.Ref.Sha = change.FromHash
}
return dst
}
func convertSignature(actor *user) scm.Signature {
return scm.Signature{
Name: actor.DisplayName,
Email: actor.EmailAddress,
Login: actor.Slug,
Avatar: avatarLink(actor.EmailAddress),
}
}
func convertPullRequestHook(src *pullRequestHook) *scm.PullRequestHook {
repo := convertRepository(&src.PullRequest.ToRef.Repository)
pr := convertPullRequest(src.PullRequest)
sender := convertUser(src.Actor)
return &scm.PullRequestHook{
Action: scm.ActionOpen,
Repo: *repo,
PullRequest: *pr,
Sender: *sender,
}
}