// Copyright 2022 Woodpecker Authors // Copyright 2018 Drone.IO Inc. // // 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 coding import ( "encoding/json" "fmt" "io" "net/http" "regexp" "strings" "github.com/woodpecker-ci/woodpecker/server/model" ) const ( hookEvent = "X-Coding-Event" hookPush = "push" hookPR = "pull_request" hookMR = "merge_request" ) type User struct { GlobalKey string `json:"global_key"` Avatar string `json:"avatar"` } type Repository struct { Name string `json:"name"` HTTPSURL string `json:"https_url"` SSHURL string `json:"ssh_url"` WebURL string `json:"web_url"` Owner *User `json:"owner"` } type Committer struct { Email string `json:"email"` Name string `json:"name"` } type Commit struct { SHA string `json:"sha"` ShortMessage string `json:"short_message"` Committer *Committer `json:"committer"` } type PullRequest MergeRequest type MergeRequest struct { SourceBranch string `json:"source_branch"` TargetBranch string `json:"target_branch"` CommitSHA string `json:"merge_commit_sha"` Status string `json:"status"` Action string `json:"action"` Number float64 `json:"number"` Title string `json:"title"` Body string `json:"body"` WebURL string `json:"web_url"` User *User `json:"user"` } type PushHook struct { Event string `json:"event"` Repository *Repository `json:"repository"` Ref string `json:"ref"` Before string `json:"before"` After string `json:"after"` Commits []*Commit `json:"commits"` User *User `json:"user"` } type PullRequestHook struct { Event string `json:"event"` Repository *Repository `json:"repository"` PullRequest *PullRequest `json:"pull_request"` } type MergeRequestHook struct { Event string `json:"event"` Repository *Repository `json:"repository"` MergeRequest *MergeRequest `json:"merge_request"` } func parseHook(r *http.Request) (*model.Repo, *model.Pipeline, error) { raw, err := io.ReadAll(r.Body) defer r.Body.Close() if err != nil { return nil, nil, err } switch r.Header.Get(hookEvent) { case hookPush: return parsePushHook(raw) case hookPR: return parsePullRequestHook(raw) case hookMR: return parseMergeReuqestHook(raw) } return nil, nil, nil } func findLastCommit(commits []*Commit, sha string) *Commit { var lastCommit *Commit for _, commit := range commits { if commit.SHA == sha { lastCommit = commit break } } if lastCommit == nil { lastCommit = &Commit{} } if lastCommit.Committer == nil { lastCommit.Committer = &Committer{} } return lastCommit } func convertRepository(repo *Repository) (*model.Repo, error) { // tricky stuff for a team project without a team owner instead of a user owner re := regexp.MustCompile(`git@.+:([^/]+)/.+\.git`) matches := re.FindStringSubmatch(repo.SSHURL) if len(matches) != 2 { return nil, fmt.Errorf("Unable to resolve owner from ssh url %q", repo.SSHURL) } return &model.Repo{ // TODO ForgeID: repo.ID, Owner: matches[1], Name: repo.Name, FullName: projectFullName(repo.Owner.GlobalKey, repo.Name), Link: repo.WebURL, Clone: repo.HTTPSURL, SCMKind: model.RepoGit, }, nil } func parsePushHook(raw []byte) (*model.Repo, *model.Pipeline, error) { hook := &PushHook{} err := json.Unmarshal(raw, hook) if err != nil { return nil, nil, err } // no pipeline triggered when removing ref if hook.After == "0000000000000000000000000000000000000000" { return nil, nil, nil } repo, err := convertRepository(hook.Repository) if err != nil { return nil, nil, err } lastCommit := findLastCommit(hook.Commits, hook.After) pipeline := &model.Pipeline{ Event: model.EventPush, Commit: hook.After, Ref: hook.Ref, Link: fmt.Sprintf("%s/git/commit/%s", hook.Repository.WebURL, hook.After), Branch: strings.Replace(hook.Ref, "refs/heads/", "", -1), Message: lastCommit.ShortMessage, Email: lastCommit.Committer.Email, Avatar: hook.User.Avatar, Author: hook.User.GlobalKey, CloneURL: hook.Repository.HTTPSURL, } return repo, pipeline, nil } func parsePullRequestHook(raw []byte) (*model.Repo, *model.Pipeline, error) { hook := &PullRequestHook{} err := json.Unmarshal(raw, hook) if err != nil { return nil, nil, err } if hook.PullRequest.Status != "CANMERGE" || (hook.PullRequest.Action != "create" && hook.PullRequest.Action != "synchronize") { return nil, nil, nil } repo, err := convertRepository(hook.Repository) if err != nil { return nil, nil, err } pipeline := &model.Pipeline{ Event: model.EventPull, Commit: hook.PullRequest.CommitSHA, Link: hook.PullRequest.WebURL, Ref: fmt.Sprintf("refs/pull/%d/MERGE", int(hook.PullRequest.Number)), Branch: hook.PullRequest.TargetBranch, Message: hook.PullRequest.Body, Author: hook.PullRequest.User.GlobalKey, Avatar: hook.PullRequest.User.Avatar, Title: hook.PullRequest.Title, CloneURL: hook.Repository.HTTPSURL, Refspec: fmt.Sprintf("%s:%s", hook.PullRequest.SourceBranch, hook.PullRequest.TargetBranch), } return repo, pipeline, nil } func parseMergeReuqestHook(raw []byte) (*model.Repo, *model.Pipeline, error) { hook := &MergeRequestHook{} err := json.Unmarshal(raw, hook) if err != nil { return nil, nil, err } if hook.MergeRequest.Status != "CANMERGE" || (hook.MergeRequest.Action != "create" && hook.MergeRequest.Action != "synchronize") { return nil, nil, nil } repo, err := convertRepository(hook.Repository) if err != nil { return nil, nil, err } pipeline := &model.Pipeline{ Event: model.EventPull, Commit: hook.MergeRequest.CommitSHA, Link: hook.MergeRequest.WebURL, Ref: fmt.Sprintf("refs/merge/%d/MERGE", int(hook.MergeRequest.Number)), Branch: hook.MergeRequest.TargetBranch, Message: hook.MergeRequest.Body, Author: hook.MergeRequest.User.GlobalKey, Avatar: hook.MergeRequest.User.Avatar, Title: hook.MergeRequest.Title, CloneURL: hook.Repository.HTTPSURL, Refspec: fmt.Sprintf("%s:%s", hook.MergeRequest.SourceBranch, hook.MergeRequest.TargetBranch), } return repo, pipeline, nil }