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.
270 lines
6.9 KiB
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,
|
|
}
|
|
}
|