Support ChangedFiles for Github & Gitlab PRs and Gitlab pushes (#697)

This commit is contained in:
Anbraten 2022-01-17 23:46:59 +01:00 committed by GitHub
parent 50570cba5c
commit 401072abb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 849 additions and 565 deletions

View file

@ -159,7 +159,7 @@ when:
:::info :::info
This feature is currently only available for GitHub, Gitlab and Gitea. This feature is currently only available for GitHub, Gitlab and Gitea.
Pull requests aren't supported at the moment ([#697](https://github.com/woodpecker-ci/woodpecker/pull/697)). Pull requests aren't supported by gitea at the moment ([go-gitea/gitea#18228](https://github.com/go-gitea/gitea/pull/18228)).
Path conditions are ignored for tag events. Path conditions are ignored for tag events.
::: :::

View file

@ -78,7 +78,7 @@ func BlockTilQueueHasRunningItem(c *gin.Context) {
func PostHook(c *gin.Context) { func PostHook(c *gin.Context) {
_store := store.FromContext(c) _store := store.FromContext(c)
tmpRepo, build, err := server.Config.Services.Remote.Hook(c.Request) tmpRepo, build, err := server.Config.Services.Remote.Hook(c, c.Request)
if err != nil { if err != nil {
msg := "failure to parse hook" msg := "failure to parse hook"
log.Debug().Err(err).Msg(msg) log.Debug().Err(err).Msg(msg)
@ -288,6 +288,7 @@ func branchFiltered(build *model.Build, remoteYamlConfigs []*remote.FileMeta) (b
return false, nil return false, nil
} }
} }
return true, nil return true, nil
} }

View file

@ -284,7 +284,7 @@ func (c *config) Branches(ctx context.Context, u *model.User, r *model.Repo) ([]
// Hook parses the incoming Bitbucket hook and returns the Repository and // Hook parses the incoming Bitbucket hook and returns the Repository and
// Build details. If the hook is unsupported nil values are returned. // Build details. If the hook is unsupported nil values are returned.
func (c *config) Hook(req *http.Request) (*model.Repo, *model.Build, error) { func (c *config) Hook(ctx context.Context, req *http.Request) (*model.Repo, *model.Build, error) {
return parseHook(req) return parseHook(req)
} }

View file

@ -264,7 +264,7 @@ func Test_bitbucket(t *testing.T) {
req.Header = http.Header{} req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush) req.Header.Set(hookEvent, hookPush)
r, _, err := c.Hook(req) r, _, err := c.Hook(ctx, req)
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(r.FullName).Equal("user_name/repo_name") g.Assert(r.FullName).Equal("user_name/repo_name")
}) })

View file

@ -236,7 +236,7 @@ func (c *Config) Deactivate(ctx context.Context, u *model.User, r *model.Repo, l
return client.DeleteHook(r.Owner, r.Name, link) return client.DeleteHook(r.Owner, r.Name, link)
} }
func (c *Config) Hook(r *http.Request) (*model.Repo, *model.Build, error) { func (c *Config) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error) {
return parseHook(r, c.URL) return parseHook(r, c.URL)
} }

View file

@ -284,7 +284,7 @@ func (c *Coding) Branches(ctx context.Context, u *model.User, r *model.Repo) ([]
// Hook parses the post-commit hook from the Request body and returns the // Hook parses the post-commit hook from the Request body and returns the
// required data in a standard format. // required data in a standard format.
func (c *Coding) Hook(r *http.Request) (*model.Repo, *model.Build, error) { func (c *Coding) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error) {
repo, build, err := parseHook(r) repo, build, err := parseHook(r)
if build != nil { if build != nil {
build.Avatar = c.resourceLink(build.Avatar) build.Avatar = c.resourceLink(build.Avatar)

View file

@ -223,7 +223,7 @@ func Test_coding(t *testing.T) {
req.Header = http.Header{} req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush) req.Header.Set(hookEvent, hookPush)
r, _, err := c.Hook(req) r, _, err := c.Hook(ctx, req)
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(r.FullName).Equal("demo1/test1") g.Assert(r.FullName).Equal("demo1/test1")
}) })

View file

@ -442,7 +442,7 @@ func (c *Gitea) Branches(ctx context.Context, u *model.User, r *model.Repo) ([]s
// Hook parses the incoming Gitea hook and returns the Repository and Build // Hook parses the incoming Gitea hook and returns the Repository and Build
// details. If the hook is unsupported nil values are returned. // details. If the hook is unsupported nil values are returned.
func (c *Gitea) Hook(r *http.Request) (*model.Repo, *model.Build, error) { func (c *Gitea) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error) {
return parseHook(r) return parseHook(r)
} }

View file

@ -25,6 +25,7 @@ import (
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/shared/utils"
) )
// helper function that converts a Gitea repository to a Woodpecker repository. // helper function that converts a Gitea repository to a Woodpecker repository.
@ -110,15 +111,15 @@ func buildFromPush(hook *pushHook) *model.Build {
} }
func getChangedFilesFromPushHook(hook *pushHook) []string { func getChangedFilesFromPushHook(hook *pushHook) []string {
files := make([]string, 0) // assume a capacity of 4 changed files per commit
files := make([]string, 0, len(hook.Commits)*4)
for _, c := range hook.Commits { for _, c := range hook.Commits {
files = append(files, c.Added...) files = append(files, c.Added...)
files = append(files, c.Removed...) files = append(files, c.Removed...)
files = append(files, c.Modified...) files = append(files, c.Modified...)
} }
return files return utils.DedupStrings(files)
} }
// helper function that extracts the Build data from a Gitea tag hook // helper function that extracts the Build data from a Gitea tag hook

View file

@ -15,9 +15,6 @@
package github package github
import ( import (
"fmt"
"strings"
"github.com/google/go-github/v39/github" "github.com/google/go-github/v39/github"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
@ -44,7 +41,7 @@ const (
const ( const (
headRefs = "refs/pull/%d/head" // pull request unmerged headRefs = "refs/pull/%d/head" // pull request unmerged
mergeRefs = "refs/pull/%d/merge" // pull request merged with base mergeRefs = "refs/pull/%d/merge" // pull request merged with base
refspec = "%s:%s" refSpec = "%s:%s"
) )
// convertStatus is a helper function used to convert a Woodpecker status to a // convertStatus is a helper function used to convert a Woodpecker status to a
@ -85,19 +82,19 @@ func convertDesc(status model.StatusValue) string {
// structure to the common Woodpecker repository structure. // structure to the common Woodpecker repository structure.
func convertRepo(from *github.Repository, private bool) *model.Repo { func convertRepo(from *github.Repository, private bool) *model.Repo {
repo := &model.Repo{ repo := &model.Repo{
Owner: *from.Owner.Login, Name: from.GetName(),
Name: *from.Name, FullName: from.GetFullName(),
FullName: *from.FullName, Link: from.GetHTMLURL(),
Link: *from.HTMLURL, IsSCMPrivate: from.GetPrivate(),
IsSCMPrivate: *from.Private, Clone: from.GetCloneURL(),
Clone: *from.CloneURL, Branch: from.GetDefaultBranch(),
Avatar: *from.Owner.AvatarURL, Owner: from.GetOwner().GetLogin(),
Avatar: from.GetOwner().GetAvatarURL(),
Perm: convertPerm(from.GetPermissions()),
SCMKind: model.RepoGit, SCMKind: model.RepoGit,
Branch: defaultBranch,
Perm: convertPerm(from),
} }
if from.DefaultBranch != nil { if len(repo.Branch) == 0 {
repo.Branch = *from.DefaultBranch repo.Branch = defaultBranch
} }
if private { if private {
repo.IsSCMPrivate = true repo.IsSCMPrivate = true
@ -107,11 +104,11 @@ func convertRepo(from *github.Repository, private bool) *model.Repo {
// convertPerm is a helper function used to convert a GitHub repository // convertPerm is a helper function used to convert a GitHub repository
// permissions to the common Woodpecker permissions structure. // permissions to the common Woodpecker permissions structure.
func convertPerm(from *github.Repository) *model.Perm { func convertPerm(perm map[string]bool) *model.Perm {
return &model.Perm{ return &model.Perm{
Admin: from.Permissions["admin"], Admin: perm["admin"],
Push: from.Permissions["push"], Push: perm["push"],
Pull: from.Permissions["pull"], Pull: perm["pull"],
} }
} }
@ -139,143 +136,29 @@ func convertTeamList(from []*github.Organization) []*model.Team {
// to the common Woodpecker repository structure. // to the common Woodpecker repository structure.
func convertTeam(from *github.Organization) *model.Team { func convertTeam(from *github.Organization) *model.Team {
return &model.Team{ return &model.Team{
Login: *from.Login, Login: from.GetLogin(),
Avatar: *from.AvatarURL, Avatar: from.GetAvatarURL(),
} }
} }
// convertRepoHook is a helper function used to extract the Repository details // convertRepoHook is a helper function used to extract the Repository details
// from a webhook and convert to the common Woodpecker repository structure. // from a webhook and convert to the common Woodpecker repository structure.
func convertRepoHook(from *webhook) *model.Repo { func convertRepoHook(eventRepo *github.PushEventRepository) *model.Repo {
repo := &model.Repo{ repo := &model.Repo{
Owner: from.Repo.Owner.Login, Owner: eventRepo.GetOwner().GetLogin(),
Name: from.Repo.Name, Name: eventRepo.GetName(),
FullName: from.Repo.FullName, FullName: eventRepo.GetFullName(),
Link: from.Repo.HTMLURL, Link: eventRepo.GetHTMLURL(),
IsSCMPrivate: from.Repo.Private, IsSCMPrivate: eventRepo.GetPrivate(),
Clone: from.Repo.CloneURL, Clone: eventRepo.GetCloneURL(),
Branch: from.Repo.DefaultBranch, Branch: eventRepo.GetDefaultBranch(),
SCMKind: model.RepoGit, SCMKind: model.RepoGit,
} }
if repo.Branch == "" { if repo.Branch == "" {
repo.Branch = defaultBranch repo.Branch = defaultBranch
} }
if repo.Owner == "" { // legacy webhooks
repo.Owner = from.Repo.Owner.Name
}
if repo.FullName == "" { if repo.FullName == "" {
repo.FullName = repo.Owner + "/" + repo.Name repo.FullName = repo.Owner + "/" + repo.Name
} }
return repo return repo
} }
// convertPushHook is a helper function used to extract the Build details
// from a push webhook and convert to the common Woodpecker Build structure.
func convertPushHook(from *webhook) *model.Build {
files := getChangedFilesFromWebhook(from)
build := &model.Build{
Event: model.EventPush,
Commit: from.Head.ID,
Ref: from.Ref,
Link: from.Head.URL,
Branch: strings.Replace(from.Ref, "refs/heads/", "", -1),
Message: from.Head.Message,
Email: from.Head.Author.Email,
Avatar: from.Sender.Avatar,
Author: from.Sender.Login,
Remote: from.Repo.CloneURL,
Sender: from.Sender.Login,
ChangedFiles: files,
}
if len(build.Author) == 0 {
build.Author = from.Head.Author.Username
}
// if len(build.Email) == 0 {
// TODO: default to gravatar?
// }
if strings.HasPrefix(build.Ref, "refs/tags/") {
// just kidding, this is actually a tag event. Why did this come as a push
// event we'll never know!
build.Event = model.EventTag
// For tags, if the base_ref (tag's base branch) is set, we're using it
// as build's branch so that we can filter events base on it
if strings.HasPrefix(from.BaseRef, "refs/heads/") {
build.Branch = strings.Replace(from.BaseRef, "refs/heads/", "", -1)
}
// tags should not have changed files
build.ChangedFiles = nil
}
return build
}
// convertPushHook is a helper function used to extract the Build details
// from a deploy webhook and convert to the common Woodpecker Build structure.
func convertDeployHook(from *webhook) *model.Build {
build := &model.Build{
Event: model.EventDeploy,
Commit: from.Deployment.Sha,
Link: from.Deployment.URL,
Message: from.Deployment.Desc,
Avatar: from.Sender.Avatar,
Author: from.Sender.Login,
Ref: from.Deployment.Ref,
Branch: from.Deployment.Ref,
Deploy: from.Deployment.Env,
Sender: from.Sender.Login,
}
// if the ref is a sha or short sha we need to manuallyconstruct the ref.
if strings.HasPrefix(build.Commit, build.Ref) || build.Commit == build.Ref {
build.Branch = from.Repo.DefaultBranch
if build.Branch == "" {
build.Branch = defaultBranch
}
build.Ref = fmt.Sprintf("refs/heads/%s", build.Branch)
}
// if the ref is a branch we should make sure it has refs/heads prefix
if !strings.HasPrefix(build.Ref, "refs/") { // branch or tag
build.Ref = fmt.Sprintf("refs/heads/%s", build.Branch)
}
return build
}
// convertPullHook is a helper function used to extract the Build details
// from a pull request webhook and convert to the common Woodpecker Build structure.
func convertPullHook(from *webhook, merge bool) *model.Build {
build := &model.Build{
Event: model.EventPull,
Commit: from.PullRequest.Head.SHA,
Link: from.PullRequest.HTMLURL,
Ref: fmt.Sprintf(headRefs, from.PullRequest.Number),
Branch: from.PullRequest.Base.Ref,
Message: from.PullRequest.Title,
Author: from.PullRequest.User.Login,
Avatar: from.PullRequest.User.Avatar,
Title: from.PullRequest.Title,
Sender: from.Sender.Login,
Remote: from.PullRequest.Head.Repo.CloneURL,
Refspec: fmt.Sprintf(refspec,
from.PullRequest.Head.Ref,
from.PullRequest.Base.Ref,
),
}
if merge {
build.Ref = fmt.Sprintf(mergeRefs, from.PullRequest.Number)
}
return build
}
func getChangedFilesFromWebhook(from *webhook) []string {
var files []string
files = append(files, from.Head.Added...)
files = append(files, from.Head.Removed...)
files = append(files, from.Head.Modified...)
if len(files) == 0 {
files = make([]string, 0)
}
return files
}

View file

@ -129,7 +129,7 @@ func Test_helper(t *testing.T) {
}, },
} }
to := convertPerm(from) to := convertPerm(from.GetPermissions())
g.Assert(to.Push).IsTrue() g.Assert(to.Push).IsTrue()
g.Assert(to.Pull).IsTrue() g.Assert(to.Pull).IsTrue()
g.Assert(to.Admin).IsTrue() g.Assert(to.Admin).IsTrue()
@ -158,124 +158,144 @@ func Test_helper(t *testing.T) {
}) })
g.It("should convert a repository from webhook", func() { g.It("should convert a repository from webhook", func() {
from := &webhook{} from := &github.PushEventRepository{Owner: &github.User{}}
from.Repo.Owner.Login = "octocat" from.Owner.Login = github.String("octocat")
from.Repo.Owner.Name = "octocat" from.Owner.Name = github.String("octocat")
from.Repo.Name = "hello-world" from.Name = github.String("hello-world")
from.Repo.FullName = "octocat/hello-world" from.FullName = github.String("octocat/hello-world")
from.Repo.Private = true from.Private = github.Bool(true)
from.Repo.HTMLURL = "https://github.com/octocat/hello-world" from.HTMLURL = github.String("https://github.com/octocat/hello-world")
from.Repo.CloneURL = "https://github.com/octocat/hello-world.git" from.CloneURL = github.String("https://github.com/octocat/hello-world.git")
from.Repo.DefaultBranch = "develop" from.DefaultBranch = github.String("develop")
repo := convertRepoHook(from) repo := convertRepoHook(from)
g.Assert(repo.Owner).Equal(from.Repo.Owner.Login) g.Assert(repo.Owner).Equal(*from.Owner.Login)
g.Assert(repo.Name).Equal(from.Repo.Name) g.Assert(repo.Name).Equal(*from.Name)
g.Assert(repo.FullName).Equal(from.Repo.FullName) g.Assert(repo.FullName).Equal(*from.FullName)
g.Assert(repo.IsSCMPrivate).Equal(from.Repo.Private) g.Assert(repo.IsSCMPrivate).Equal(*from.Private)
g.Assert(repo.Link).Equal(from.Repo.HTMLURL) g.Assert(repo.Link).Equal(*from.HTMLURL)
g.Assert(repo.Clone).Equal(from.Repo.CloneURL) g.Assert(repo.Clone).Equal(*from.CloneURL)
g.Assert(repo.Branch).Equal(from.Repo.DefaultBranch) g.Assert(repo.Branch).Equal(*from.DefaultBranch)
}) })
g.It("should convert a pull request from webhook", func() { g.It("should convert a pull request from webhook", func() {
from := &webhook{} from := &github.PullRequestEvent{
from.PullRequest.Base.Ref = "master" Action: github.String(actionOpen),
from.PullRequest.Head.Ref = "changes" PullRequest: &github.PullRequest{
from.PullRequest.Head.SHA = "f72fc19" State: github.String(stateOpen),
from.PullRequest.Head.Repo.CloneURL = "https://github.com/octocat/hello-world-fork" HTMLURL: github.String("https://github.com/octocat/hello-world/pulls/42"),
from.PullRequest.HTMLURL = "https://github.com/octocat/hello-world/pulls/42" Number: github.Int(42),
from.PullRequest.Number = 42 Title: github.String("Updated README.md"),
from.PullRequest.Title = "Updated README.md" Base: &github.PullRequestBranch{
from.PullRequest.User.Login = "octocat" Ref: github.String("master"),
from.PullRequest.User.Avatar = "https://avatars1.githubusercontent.com/u/583231" },
from.Sender.Login = "octocat" Head: &github.PullRequestBranch{
Ref: github.String("changes"),
build := convertPullHook(from, true) SHA: github.String("f72fc19"),
Repo: &github.Repository{
CloneURL: github.String("https://github.com/octocat/hello-world-fork"),
},
},
User: &github.User{
Login: github.String("octocat"),
AvatarURL: github.String("https://avatars1.githubusercontent.com/u/583231"),
},
}, Sender: &github.User{
Login: github.String("octocat"),
},
}
pull, _, build, err := parsePullHook(from, true, false)
g.Assert(err).IsNil()
g.Assert(pull).IsNotNil()
g.Assert(build.Event).Equal(model.EventPull) g.Assert(build.Event).Equal(model.EventPull)
g.Assert(build.Branch).Equal(from.PullRequest.Base.Ref) g.Assert(build.Branch).Equal(*from.PullRequest.Base.Ref)
g.Assert(build.Ref).Equal("refs/pull/42/merge") g.Assert(build.Ref).Equal("refs/pull/42/merge")
g.Assert(build.Refspec).Equal("changes:master") g.Assert(build.Refspec).Equal("changes:master")
g.Assert(build.Remote).Equal("https://github.com/octocat/hello-world-fork") g.Assert(build.Remote).Equal("https://github.com/octocat/hello-world-fork")
g.Assert(build.Commit).Equal(from.PullRequest.Head.SHA) g.Assert(build.Commit).Equal(*from.PullRequest.Head.SHA)
g.Assert(build.Message).Equal(from.PullRequest.Title) g.Assert(build.Message).Equal(*from.PullRequest.Title)
g.Assert(build.Title).Equal(from.PullRequest.Title) g.Assert(build.Title).Equal(*from.PullRequest.Title)
g.Assert(build.Author).Equal(from.PullRequest.User.Login) g.Assert(build.Author).Equal(*from.PullRequest.User.Login)
g.Assert(build.Avatar).Equal(from.PullRequest.User.Avatar) g.Assert(build.Avatar).Equal(*from.PullRequest.User.AvatarURL)
g.Assert(build.Sender).Equal(from.Sender.Login) g.Assert(build.Sender).Equal(*from.Sender.Login)
}) })
g.It("should convert a deployment from webhook", func() { g.It("should convert a deployment from webhook", func() {
from := &webhook{} from := &github.DeploymentEvent{Deployment: &github.Deployment{}, Sender: &github.User{}}
from.Deployment.Desc = ":shipit:" from.Deployment.Description = github.String(":shipit:")
from.Deployment.Env = "production" from.Deployment.Environment = github.String("production")
from.Deployment.ID = 42 from.Deployment.ID = github.Int64(42)
from.Deployment.Ref = "master" from.Deployment.Ref = github.String("master")
from.Deployment.Sha = "f72fc19" from.Deployment.SHA = github.String("f72fc19")
from.Deployment.URL = "https://github.com/octocat/hello-world" from.Deployment.URL = github.String("https://github.com/octocat/hello-world")
from.Sender.Login = "octocat" from.Sender.Login = github.String("octocat")
from.Sender.Avatar = "https://avatars1.githubusercontent.com/u/583231" from.Sender.AvatarURL = github.String("https://avatars1.githubusercontent.com/u/583231")
build := convertDeployHook(from) _, build, err := parseDeployHook(from, false)
g.Assert(err).IsNil()
g.Assert(build.Event).Equal(model.EventDeploy) g.Assert(build.Event).Equal(model.EventDeploy)
g.Assert(build.Branch).Equal("master") g.Assert(build.Branch).Equal("master")
g.Assert(build.Ref).Equal("refs/heads/master") g.Assert(build.Ref).Equal("refs/heads/master")
g.Assert(build.Commit).Equal(from.Deployment.Sha) g.Assert(build.Commit).Equal(*from.Deployment.SHA)
g.Assert(build.Message).Equal(from.Deployment.Desc) g.Assert(build.Message).Equal(*from.Deployment.Description)
g.Assert(build.Link).Equal(from.Deployment.URL) g.Assert(build.Link).Equal(*from.Deployment.URL)
g.Assert(build.Author).Equal(from.Sender.Login) g.Assert(build.Author).Equal(*from.Sender.Login)
g.Assert(build.Avatar).Equal(from.Sender.Avatar) g.Assert(build.Avatar).Equal(*from.Sender.AvatarURL)
}) })
g.It("should convert a push from webhook", func() { g.It("should convert a push from webhook", func() {
from := &webhook{} from := &github.PushEvent{Sender: &github.User{}, Repo: &github.PushEventRepository{}, HeadCommit: &github.HeadCommit{Author: &github.CommitAuthor{}}}
from.Sender.Login = "octocat" from.Sender.Login = github.String("octocat")
from.Sender.Avatar = "https://avatars1.githubusercontent.com/u/583231" from.Sender.AvatarURL = github.String("https://avatars1.githubusercontent.com/u/583231")
from.Repo.CloneURL = "https://github.com/octocat/hello-world.git" from.Repo.CloneURL = github.String("https://github.com/octocat/hello-world.git")
from.Head.Author.Email = "octocat@github.com" from.HeadCommit.Author.Email = github.String("github.String(octocat@github.com")
from.Head.Message = "updated README.md" from.HeadCommit.Message = github.String("updated README.md")
from.Head.URL = "https://github.com/octocat/hello-world" from.HeadCommit.URL = github.String("https://github.com/octocat/hello-world")
from.Head.ID = "f72fc19" from.HeadCommit.ID = github.String("f72fc19")
from.Ref = "refs/heads/master" from.Ref = github.String("refs/heads/master")
build := convertPushHook(from) _, build, err := parsePushHook(from)
g.Assert(err).IsNil()
g.Assert(build.Event).Equal(model.EventPush) g.Assert(build.Event).Equal(model.EventPush)
g.Assert(build.Branch).Equal("master") g.Assert(build.Branch).Equal("master")
g.Assert(build.Ref).Equal("refs/heads/master") g.Assert(build.Ref).Equal("refs/heads/master")
g.Assert(build.Commit).Equal(from.Head.ID) g.Assert(build.Commit).Equal(*from.HeadCommit.ID)
g.Assert(build.Message).Equal(from.Head.Message) g.Assert(build.Message).Equal(*from.HeadCommit.Message)
g.Assert(build.Link).Equal(from.Head.URL) g.Assert(build.Link).Equal(*from.HeadCommit.URL)
g.Assert(build.Author).Equal(from.Sender.Login) g.Assert(build.Author).Equal(*from.Sender.Login)
g.Assert(build.Avatar).Equal(from.Sender.Avatar) g.Assert(build.Avatar).Equal(*from.Sender.AvatarURL)
g.Assert(build.Email).Equal(from.Head.Author.Email) g.Assert(build.Email).Equal(*from.HeadCommit.Author.Email)
g.Assert(build.Remote).Equal(from.Repo.CloneURL) g.Assert(build.Remote).Equal(*from.Repo.CloneURL)
}) })
g.It("should convert a tag from webhook", func() { g.It("should convert a tag from webhook", func() {
from := &webhook{} from := &github.PushEvent{}
from.Ref = "refs/tags/v1.0.0" from.Ref = github.String("refs/tags/v1.0.0")
build := convertPushHook(from) _, build, err := parsePushHook(from)
g.Assert(err).IsNil()
g.Assert(build.Event).Equal(model.EventTag) g.Assert(build.Event).Equal(model.EventTag)
g.Assert(build.Ref).Equal("refs/tags/v1.0.0") g.Assert(build.Ref).Equal("refs/tags/v1.0.0")
}) })
g.It("should convert tag's base branch from webhook to build's branch ", func() { g.It("should convert tag's base branch from webhook to build's branch ", func() {
from := &webhook{} from := &github.PushEvent{}
from.Ref = "refs/tags/v1.0.0" from.Ref = github.String("refs/tags/v1.0.0")
from.BaseRef = "refs/heads/master" from.BaseRef = github.String("refs/heads/master")
build := convertPushHook(from) _, build, err := parsePushHook(from)
g.Assert(err).IsNil()
g.Assert(build.Event).Equal(model.EventTag) g.Assert(build.Event).Equal(model.EventTag)
g.Assert(build.Branch).Equal("master") g.Assert(build.Branch).Equal("master")
}) })
g.It("should not convert tag's base_ref from webhook if not prefixed with 'ref/heads/'", func() { g.It("should not convert tag's base_ref from webhook if not prefixed with 'ref/heads/'", func() {
from := &webhook{} from := &github.PushEvent{}
from.Ref = "refs/tags/v1.0.0" from.Ref = github.String("refs/tags/v1.0.0")
from.BaseRef = "refs/refs/master" from.BaseRef = github.String("refs/refs/master")
build := convertPushHook(from) _, build, err := parsePushHook(from)
g.Assert(err).IsNil()
g.Assert(build.Event).Equal(model.EventTag) g.Assert(build.Event).Equal(model.EventTag)
g.Assert(build.Branch).Equal("refs/tags/v1.0.0") g.Assert(build.Branch).Equal("refs/tags/v1.0.0")
}) })

View file

@ -16,55 +16,230 @@ package fixtures
// HookPush is a sample push hook. // HookPush is a sample push hook.
// https://developer.github.com/v3/activity/events/types/#pushevent // https://developer.github.com/v3/activity/events/types/#pushevent
const HookPush = ` const HookPush = `{
{ "ref": "refs/heads/master",
"ref": "refs/heads/changes", "before": "2f780193b136b72bfea4eeb640786a8c4450c7a2",
"created": false, "after": "366701fde727cb7a9e7f21eb88264f59f6f9b89c",
"deleted": false,
"head_commit": {
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"message": "Update README.md",
"timestamp": "2015-05-05T19:40:15-04:00",
"url": "https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"author": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com",
"username": "baxterthehacker"
},
"committer": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com",
"username": "baxterthehacker"
},
"added": ["CHANGELOG.md"],
"removed": [],
"modified": ["app/controller/application.rb"]
},
"repository": { "repository": {
"id": 35129377, "id": 179344069,
"name": "public-repo", "node_id": "MDEwOlJlcG9zaXRvcnkxNzkzNDQwNjk=",
"full_name": "baxterthehacker/public-repo", "name": "woodpecker",
"owner": { "full_name": "woodpecker-ci/woodpecker",
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com"
},
"private": false, "private": false,
"html_url": "https://github.com/baxterthehacker/public-repo", "owner": {
"default_branch": "master" "name": "woodpecker-ci",
"email": null,
"login": "woodpecker-ci",
"id": 84780935,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjg0NzgwOTM1",
"avatar_url": "https://avatars.githubusercontent.com/u/84780935?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/woodpecker-ci",
"html_url": "https://github.com/woodpecker-ci",
"followers_url": "https://api.github.com/users/woodpecker-ci/followers",
"following_url": "https://api.github.com/users/woodpecker-ci/following{/other_user}",
"gists_url": "https://api.github.com/users/woodpecker-ci/gists{/gist_id}",
"starred_url": "https://api.github.com/users/woodpecker-ci/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/woodpecker-ci/subscriptions",
"organizations_url": "https://api.github.com/users/woodpecker-ci/orgs",
"repos_url": "https://api.github.com/users/woodpecker-ci/repos",
"events_url": "https://api.github.com/users/woodpecker-ci/events{/privacy}",
"received_events_url": "https://api.github.com/users/woodpecker-ci/received_events",
"type": "Organization",
"site_admin": false
},
"html_url": "https://github.com/woodpecker-ci/woodpecker",
"description": "Woodpecker is a community fork of the Drone CI system.",
"fork": false,
"url": "https://github.com/woodpecker-ci/woodpecker",
"forks_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/forks",
"keys_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/teams",
"hooks_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/hooks",
"issue_events_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/issues/events{/number}",
"events_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/events",
"assignees_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/assignees{/user}",
"branches_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/branches{/branch}",
"tags_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/tags",
"blobs_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/statuses/{sha}",
"languages_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/languages",
"stargazers_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/stargazers",
"contributors_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/contributors",
"subscribers_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/subscribers",
"subscription_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/subscription",
"commits_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/contents/{+path}",
"compare_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/merges",
"archive_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/downloads",
"issues_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/issues{/number}",
"pulls_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/pulls{/number}",
"milestones_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/milestones{/number}",
"notifications_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/labels{/name}",
"releases_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/releases{/id}",
"deployments_url": "https://api.github.com/repos/woodpecker-ci/woodpecker/deployments",
"created_at": 1554314798,
"updated_at": "2022-01-16T20:19:33Z",
"pushed_at": 1642370257,
"git_url": "git://github.com/woodpecker-ci/woodpecker.git",
"ssh_url": "git@github.com:woodpecker-ci/woodpecker.git",
"clone_url": "https://github.com/woodpecker-ci/woodpecker.git",
"svn_url": "https://github.com/woodpecker-ci/woodpecker",
"homepage": "https://woodpecker-ci.org",
"size": 81324,
"stargazers_count": 659,
"watchers_count": 659,
"language": "Go",
"has_issues": true,
"has_projects": false,
"has_downloads": true,
"has_wiki": false,
"has_pages": false,
"forks_count": 84,
"mirror_url": null,
"archived": false,
"disabled": false,
"open_issues_count": 123,
"license": {
"key": "apache-2.0",
"name": "Apache License 2.0",
"spdx_id": "Apache-2.0",
"url": "https://api.github.com/licenses/apache-2.0",
"node_id": "MDc6TGljZW5zZTI="
},
"allow_forking": true,
"is_template": false,
"topics": [
"ci",
"devops",
"docker",
"hacktoberfest",
"hacktoberfest2021",
"woodpeckerci"
],
"visibility": "public",
"forks": 84,
"open_issues": 123,
"watchers": 659,
"default_branch": "master",
"stargazers": 659,
"master_branch": "master",
"organization": "woodpecker-ci"
}, },
"pusher": { "pusher": {
"name": "baxterthehacker", "name": "6543",
"email": "baxterthehacker@users.noreply.github.com" "email": "noreply@6543.de"
},
"organization": {
"login": "woodpecker-ci",
"id": 84780935,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjg0NzgwOTM1",
"url": "https://api.github.com/orgs/woodpecker-ci",
"repos_url": "https://api.github.com/orgs/woodpecker-ci/repos",
"events_url": "https://api.github.com/orgs/woodpecker-ci/events",
"hooks_url": "https://api.github.com/orgs/woodpecker-ci/hooks",
"issues_url": "https://api.github.com/orgs/woodpecker-ci/issues",
"members_url": "https://api.github.com/orgs/woodpecker-ci/members{/member}",
"public_members_url": "https://api.github.com/orgs/woodpecker-ci/public_members{/member}",
"avatar_url": "https://avatars.githubusercontent.com/u/84780935?v=4",
"description": "Woodpecker is a community fork of the Drone CI system."
}, },
"sender": { "sender": {
"login": "baxterthehacker", "login": "6543",
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3" "id": 24977596,
} "node_id": "MDQ6VXNlcjI0OTc3NTk2",
} "avatar_url": "https://avatars.githubusercontent.com/u/24977596?v=4",
` "gravatar_id": "",
"url": "https://api.github.com/users/6543",
"html_url": "https://github.com/6543",
"followers_url": "https://api.github.com/users/6543/followers",
"following_url": "https://api.github.com/users/6543/following{/other_user}",
"gists_url": "https://api.github.com/users/6543/gists{/gist_id}",
"starred_url": "https://api.github.com/users/6543/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/6543/subscriptions",
"organizations_url": "https://api.github.com/users/6543/orgs",
"repos_url": "https://api.github.com/users/6543/repos",
"events_url": "https://api.github.com/users/6543/events{/privacy}",
"received_events_url": "https://api.github.com/users/6543/received_events",
"type": "User",
"site_admin": false
},
"created": false,
"deleted": false,
"forced": false,
"base_ref": null,
"compare": "https://github.com/woodpecker-ci/woodpecker/compare/2f780193b136...366701fde727",
"commits": [
{
"id": "366701fde727cb7a9e7f21eb88264f59f6f9b89c",
"tree_id": "638e046f1e1e15dbed1ddf40f9471bf1af4d64ce",
"distinct": true,
"message": "Fix multiline secrets replacer (#700)\n\n* Fix multiline secrets replacer\r\n\r\n* Add tests",
"timestamp": "2022-01-16T22:57:37+01:00",
"url": "https://github.com/woodpecker-ci/woodpecker/commit/366701fde727cb7a9e7f21eb88264f59f6f9b89c",
"author": {
"name": "Philipp",
"email": "noreply@philipp.xzy",
"username": "nupplaphil"
},
"committer": {
"name": "GitHub",
"email": "noreply@github.com",
"username": "web-flow"
},
"added": [
// HookPush is a sample push hook that is marked as deleted, and is expected to ],
// be ignored. "removed": [
],
"modified": [
"pipeline/shared/replace_secrets.go",
"pipeline/shared/replace_secrets_test.go"
]
}
],
"head_commit": {
"id": "366701fde727cb7a9e7f21eb88264f59f6f9b89c",
"tree_id": "638e046f1e1e15dbed1ddf40f9471bf1af4d64ce",
"distinct": true,
"message": "Fix multiline secrets replacer (#700)\n\n* Fix multiline secrets replacer\r\n\r\n* Add tests",
"timestamp": "2022-01-16T22:57:37+01:00",
"url": "https://github.com/woodpecker-ci/woodpecker/commit/366701fde727cb7a9e7f21eb88264f59f6f9b89c",
"author": {
"name": "Philipp",
"email": "admin@philipp.info",
"username": "nupplaphil"
},
"committer": {
"name": "GitHub",
"email": "noreply@github.com",
"username": "web-flow"
},
"added": [
],
"removed": [
],
"modified": [
"pipeline/shared/replace_secrets.go",
"pipeline/shared/replace_secrets_test.go"
]
}
}`
// HookPushDeleted is a sample push hook that is marked as deleted, and is expected to be ignored.
const HookPushDeleted = ` const HookPushDeleted = `
{ {
"deleted": true "deleted": true

View file

@ -0,0 +1 @@
package fixtures

View file

@ -26,12 +26,15 @@ import (
"strings" "strings"
"github.com/google/go-github/v39/github" "github.com/google/go-github/v39/github"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/common" "github.com/woodpecker-ci/woodpecker/server/remote/common"
"github.com/woodpecker-ci/woodpecker/server/store"
"github.com/woodpecker-ci/woodpecker/shared/utils"
) )
const ( const (
@ -219,7 +222,7 @@ func (c *client) Perm(ctx context.Context, u *model.User, r *model.Repo) (*model
if err != nil { if err != nil {
return nil, err return nil, err
} }
return convertPerm(repo), nil return convertPerm(repo.GetPermissions()), nil
} }
// File fetches the file from the GitHub repository and returns its contents. // File fetches the file from the GitHub repository and returns its contents.
@ -491,6 +494,54 @@ func (c *client) Branches(ctx context.Context, u *model.User, r *model.Repo) ([]
// Hook parses the post-commit hook from the Request body // Hook parses the post-commit hook from the Request body
// and returns the required data in a standard format. // and returns the required data in a standard format.
func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) { func (c *client) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error) {
return parseHook(r, c.MergeRef) pull, repo, build, err := parseHook(r, c.MergeRef, c.PrivateMode)
if err != nil {
return nil, nil, err
}
if pull != nil && len(build.ChangedFiles) == 0 {
build, err = c.loadChangedFilesFromPullRequest(ctx, pull, repo, build)
if err != nil {
return nil, nil, err
}
}
return repo, build, nil
}
func (c *client) loadChangedFilesFromPullRequest(ctx context.Context, pull *github.PullRequest, tmpRepo *model.Repo, build *model.Build) (*model.Build, error) {
_store, ok := store.TryFromContext(ctx)
if !ok {
log.Error().Msg("could not get store from context")
return build, nil
}
repo, err := _store.GetRepoName(tmpRepo.Owner + "/" + tmpRepo.Name)
if err != nil {
return nil, err
}
user, err := _store.GetUser(repo.UserID)
if err != nil {
return nil, err
}
opts := &github.ListOptions{Page: 1}
fileList := make([]string, 0, 16)
for opts.Page > 0 {
files, resp, err := c.newClientToken(ctx, user.Token).PullRequests.ListFiles(ctx, repo.Owner, repo.Name, pull.GetNumber(), opts)
if err != nil {
return nil, err
}
for _, file := range files {
fileList = append(fileList, file.GetFilename(), file.GetPreviousFilename())
}
opts.Page = resp.NextPage
}
build.ChangedFiles = utils.DedupStrings(fileList)
return build, nil
} }

View file

@ -16,20 +16,20 @@ package github
import ( import (
"bytes" "bytes"
"encoding/json" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings"
"github.com/google/go-github/v39/github"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/shared/utils"
) )
const ( const (
hookEvent = "X-Github-Event" hookField = "payload"
hookField = "payload"
hookDeploy = "deployment"
hookPush = "push"
hookPull = "pull_request"
actionOpen = "opened" actionOpen = "opened"
actionSync = "synchronize" actionSync = "synchronize"
@ -39,7 +39,7 @@ const (
// parseHook parses a GitHub hook from an http.Request request and returns // parseHook parses a GitHub hook from an http.Request request and returns
// Repo and Build detail. If a hook type is unsupported nil values are returned. // Repo and Build detail. If a hook type is unsupported nil values are returned.
func parseHook(r *http.Request, merge bool) (*model.Repo, *model.Build, error) { func parseHook(r *http.Request, merge, privateMode bool) (*github.PullRequest, *model.Repo, *model.Build, error) {
var reader io.Reader = r.Body var reader io.Reader = r.Body
if payload := r.FormValue(hookField); payload != "" { if payload := r.FormValue(hookField); payload != "" {
@ -48,59 +48,143 @@ func parseHook(r *http.Request, merge bool) (*model.Repo, *model.Build, error) {
raw, err := ioutil.ReadAll(reader) raw, err := ioutil.ReadAll(reader)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, nil, err
} }
switch r.Header.Get(hookEvent) { payload, err := github.ParseWebHook(github.WebHookType(r), raw)
case hookPush: if err != nil {
return parsePushHook(raw) return nil, nil, nil, err
case hookDeploy:
return parseDeployHook(raw)
case hookPull:
return parsePullHook(raw, merge)
} }
return nil, nil, nil
switch hook := payload.(type) {
case *github.PushEvent:
repo, build, err := parsePushHook(hook)
return nil, repo, build, err
case *github.DeploymentEvent:
repo, build, err := parseDeployHook(hook, privateMode)
return nil, repo, build, err
case *github.PullRequestEvent:
return parsePullHook(hook, merge, privateMode)
}
return nil, nil, nil, nil
} }
// parsePushHook parses a push hook and returns the Repo and Build details. // parsePushHook parses a push hook and returns the Repo and Build details.
// If the commit type is unsupported nil values are returned. // If the commit type is unsupported nil values are returned.
func parsePushHook(payload []byte) (*model.Repo, *model.Build, error) { func parsePushHook(hook *github.PushEvent) (*model.Repo, *model.Build, error) {
hook := new(webhook) if hook.Deleted != nil && *hook.Deleted {
err := json.Unmarshal(payload, hook) return nil, nil, nil
if err != nil {
return nil, nil, err
} }
if hook.Deleted {
return nil, nil, err build := &model.Build{
Event: model.EventPush,
Commit: hook.GetHeadCommit().GetID(),
Ref: hook.GetRef(),
Link: hook.GetHeadCommit().GetURL(),
Branch: strings.Replace(hook.GetRef(), "refs/heads/", "", -1),
Message: hook.GetHeadCommit().GetMessage(),
Email: hook.GetHeadCommit().GetAuthor().GetEmail(),
Avatar: hook.GetSender().GetAvatarURL(),
Author: hook.GetSender().GetLogin(),
Remote: hook.GetRepo().GetCloneURL(),
Sender: hook.GetSender().GetLogin(),
ChangedFiles: getChangedFilesFromCommits(hook.Commits),
} }
return convertRepoHook(hook), convertPushHook(hook), nil
if len(build.Author) == 0 {
build.Author = hook.GetHeadCommit().GetAuthor().GetLogin()
}
// if len(build.Email) == 0 {
// TODO: default to gravatar?
// }
if strings.HasPrefix(build.Ref, "refs/tags/") {
// just kidding, this is actually a tag event. Why did this come as a push
// event we'll never know!
build.Event = model.EventTag
build.ChangedFiles = nil
// For tags, if the base_ref (tag's base branch) is set, we're using it
// as build's branch so that we can filter events base on it
if strings.HasPrefix(hook.GetBaseRef(), "refs/heads/") {
build.Branch = strings.Replace(hook.GetBaseRef(), "refs/heads/", "", -1)
}
}
return convertRepoHook(hook.GetRepo()), build, nil
} }
// parseDeployHook parses a deployment and returns the Repo and Build details. // parseDeployHook parses a deployment and returns the Repo and Build details.
// If the commit type is unsupported nil values are returned. // If the commit type is unsupported nil values are returned.
func parseDeployHook(payload []byte) (*model.Repo, *model.Build, error) { func parseDeployHook(hook *github.DeploymentEvent, privateMode bool) (*model.Repo, *model.Build, error) {
hook := new(webhook) build := &model.Build{
if err := json.Unmarshal(payload, hook); err != nil { Event: model.EventDeploy,
return nil, nil, err Commit: hook.GetDeployment().GetSHA(),
Link: hook.GetDeployment().GetURL(),
Message: hook.GetDeployment().GetDescription(),
Ref: hook.GetDeployment().GetRef(),
Branch: hook.GetDeployment().GetRef(),
Deploy: hook.GetDeployment().GetEnvironment(),
Avatar: hook.GetSender().GetAvatarURL(),
Author: hook.GetSender().GetLogin(),
Sender: hook.GetSender().GetLogin(),
} }
return convertRepoHook(hook), convertDeployHook(hook), nil // if the ref is a sha or short sha we need to manually construct the ref.
if strings.HasPrefix(build.Commit, build.Ref) || build.Commit == build.Ref {
build.Branch = hook.GetRepo().GetDefaultBranch()
if build.Branch == "" {
build.Branch = defaultBranch
}
build.Ref = fmt.Sprintf("refs/heads/%s", build.Branch)
}
// if the ref is a branch we should make sure it has refs/heads prefix
if !strings.HasPrefix(build.Ref, "refs/") { // branch or tag
build.Ref = fmt.Sprintf("refs/heads/%s", build.Branch)
}
return convertRepo(hook.GetRepo(), privateMode), build, nil
} }
// parsePullHook parses a pull request hook and returns the Repo and Build // parsePullHook parses a pull request hook and returns the Repo and Build
// details. If the pull request is closed nil values are returned. // details. If the pull request is closed nil values are returned.
func parsePullHook(payload []byte, merge bool) (*model.Repo, *model.Build, error) { func parsePullHook(hook *github.PullRequestEvent, merge, privateMode bool) (*github.PullRequest, *model.Repo, *model.Build, error) {
hook := new(webhook) // only listen to new merge-requests and pushes to open ones
err := json.Unmarshal(payload, hook) if hook.GetAction() != actionOpen && hook.GetAction() != actionSync {
if err != nil { return nil, nil, nil, nil
return nil, nil, err }
if hook.GetPullRequest().GetState() != stateOpen {
return nil, nil, nil, nil
} }
// ignore these build := &model.Build{
if hook.Action != actionOpen && hook.Action != actionSync { Event: model.EventPull,
return nil, nil, nil Commit: hook.GetPullRequest().GetHead().GetSHA(),
Link: hook.GetPullRequest().GetHTMLURL(),
Ref: fmt.Sprintf(headRefs, hook.GetPullRequest().GetNumber()),
Branch: hook.GetPullRequest().GetBase().GetRef(),
Message: hook.GetPullRequest().GetTitle(),
Author: hook.GetPullRequest().GetUser().GetLogin(),
Avatar: hook.GetPullRequest().GetUser().GetAvatarURL(),
Title: hook.GetPullRequest().GetTitle(),
Sender: hook.GetSender().GetLogin(),
Remote: hook.GetPullRequest().GetHead().GetRepo().GetCloneURL(),
Refspec: fmt.Sprintf(refSpec,
hook.GetPullRequest().GetHead().GetRef(),
hook.GetPullRequest().GetBase().GetRef(),
),
} }
if hook.PullRequest.State != stateOpen { if merge {
return nil, nil, nil build.Ref = fmt.Sprintf(mergeRefs, hook.GetPullRequest().GetNumber())
} }
return convertRepoHook(hook), convertPullHook(hook, merge), nil
return hook.GetPullRequest(), convertRepo(hook.GetRepo(), privateMode), build, nil
}
func getChangedFilesFromCommits(commits []*github.HeadCommit) []string {
// assume a capacity of 4 changed files per commit
files := make([]string, 0, len(commits)*4)
for _, cm := range commits {
files = append(files, cm.Added...)
files = append(files, cm.Removed...)
files = append(files, cm.Modified...)
}
return utils.DedupStrings(files)
} }

View file

@ -17,6 +17,7 @@ package github
import ( import (
"bytes" "bytes"
"net/http" "net/http"
"sort"
"testing" "testing"
"github.com/franela/goblin" "github.com/franela/goblin"
@ -25,85 +26,91 @@ import (
"github.com/woodpecker-ci/woodpecker/server/remote/github/fixtures" "github.com/woodpecker-ci/woodpecker/server/remote/github/fixtures"
) )
const (
hookEvent = "X-Github-Event"
hookDeploy = "deployment"
hookPush = "push"
hookPull = "pull_request"
)
func testHookRequest(payload []byte, event string) *http.Request {
buf := bytes.NewBuffer(payload)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, event)
return req
}
func Test_parser(t *testing.T) { func Test_parser(t *testing.T) {
g := goblin.Goblin(t) g := goblin.Goblin(t)
g.Describe("GitHub parser", func() { g.Describe("GitHub parser", func() {
g.It("should ignore unsupported hook events", func() { g.It("should ignore unsupported hook events", func() {
buf := bytes.NewBufferString(fixtures.HookPullRequest) req := testHookRequest([]byte(fixtures.HookPullRequest), "issues")
req, _ := http.NewRequest("POST", "/hook", buf) p, r, b, err := parseHook(req, false, false)
req.Header = http.Header{}
req.Header.Set(hookEvent, "issues")
r, b, err := parseHook(req, false)
g.Assert(r).IsNil() g.Assert(r).IsNil()
g.Assert(b).IsNil() g.Assert(b).IsNil()
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(p).IsNil()
}) })
g.Describe("given a push hook", func() { g.Describe("given a push hook", func() {
g.It("should skip when action is deleted", func() { g.It("should skip when action is deleted", func() {
raw := []byte(fixtures.HookPushDeleted) req := testHookRequest([]byte(fixtures.HookPushDeleted), hookPush)
r, b, err := parsePushHook(raw) p, r, b, err := parseHook(req, false, false)
g.Assert(r).IsNil() g.Assert(r).IsNil()
g.Assert(b).IsNil() g.Assert(b).IsNil()
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(p).IsNil()
}) })
g.It("should extract repository and build details", func() { g.It("should extract repository and build details", func() {
buf := bytes.NewBufferString(fixtures.HookPush) req := testHookRequest([]byte(fixtures.HookPush), hookPush)
req, _ := http.NewRequest("POST", "/hook", buf) p, r, b, err := parseHook(req, false, false)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush)
r, b, err := parseHook(req, false)
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(p).IsNil()
g.Assert(r).IsNotNil() g.Assert(r).IsNotNil()
g.Assert(b).IsNotNil() g.Assert(b).IsNotNil()
g.Assert(b.Event).Equal(model.EventPush) g.Assert(b.Event).Equal(model.EventPush)
expectedFiles := []string{"CHANGELOG.md", "app/controller/application.rb"} sort.Strings(b.ChangedFiles)
g.Assert(b.ChangedFiles).Equal(expectedFiles) g.Assert(b.ChangedFiles).Equal([]string{"pipeline/shared/replace_secrets.go", "pipeline/shared/replace_secrets_test.go"})
}) })
}) })
g.Describe("given a pull request hook", func() { g.Describe("given a pull request hook", func() {
g.It("should skip when action is not open or sync", func() { g.It("should skip when action is not open or sync", func() {
raw := []byte(fixtures.HookPullRequestInvalidAction) req := testHookRequest([]byte(fixtures.HookPullRequestInvalidAction), hookPull)
r, b, err := parsePullHook(raw, false) p, r, b, err := parseHook(req, false, false)
g.Assert(r).IsNil() g.Assert(r).IsNil()
g.Assert(b).IsNil() g.Assert(b).IsNil()
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(p).IsNil()
}) })
g.It("should skip when state is not open", func() { g.It("should skip when state is not open", func() {
raw := []byte(fixtures.HookPullRequestInvalidState) req := testHookRequest([]byte(fixtures.HookPullRequestInvalidState), hookPull)
r, b, err := parsePullHook(raw, false) p, r, b, err := parseHook(req, false, false)
g.Assert(r).IsNil() g.Assert(r).IsNil()
g.Assert(b).IsNil() g.Assert(b).IsNil()
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(p).IsNil()
}) })
g.It("should extract repository and build details", func() { g.It("should extract repository and build details", func() {
buf := bytes.NewBufferString(fixtures.HookPullRequest) req := testHookRequest([]byte(fixtures.HookPullRequest), hookPull)
req, _ := http.NewRequest("POST", "/hook", buf) p, r, b, err := parseHook(req, false, false)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPull)
r, b, err := parseHook(req, false)
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(r).IsNotNil() g.Assert(r).IsNotNil()
g.Assert(b).IsNotNil() g.Assert(b).IsNotNil()
g.Assert(p).IsNotNil()
g.Assert(b.Event).Equal(model.EventPull) g.Assert(b.Event).Equal(model.EventPull)
}) })
}) })
g.Describe("given a deployment hook", func() { g.Describe("given a deployment hook", func() {
g.It("should extract repository and build details", func() { g.It("should extract repository and build details", func() {
buf := bytes.NewBufferString(fixtures.HookDeploy) req := testHookRequest([]byte(fixtures.HookDeploy), hookDeploy)
req, _ := http.NewRequest("POST", "/hook", buf) p, r, b, err := parseHook(req, false, false)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookDeploy)
r, b, err := parseHook(req, false)
g.Assert(err).IsNil() g.Assert(err).IsNil()
g.Assert(r).IsNotNil() g.Assert(r).IsNotNil()
g.Assert(b).IsNotNil() g.Assert(b).IsNotNil()
g.Assert(p).IsNil()
g.Assert(b.Event).Equal(model.EventDeploy) g.Assert(b.Event).Equal(model.EventDeploy)
}) })
}) })

View file

@ -1,102 +0,0 @@
// 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 github
type webhook struct {
Ref string `json:"ref"`
Action string `json:"action"`
Deleted bool `json:"deleted"`
BaseRef string `json:"base_ref"`
Head struct {
ID string `json:"id"`
URL string `json:"url"`
Message string `json:"message"`
Timestamp string `json:"timestamp"`
Author struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
} `json:"author"`
Committer struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
} `json:"committer"`
Added []string `json:"added"`
Removed []string `json:"removed"`
Modified []string `json:"modified"`
} `json:"head_commit"`
Sender struct {
Login string `json:"login"`
Avatar string `json:"avatar_url"`
} `json:"sender"`
// repository details
Repo struct {
Owner struct {
Login string `json:"login"`
Name string `json:"name"`
} `json:"owner"`
Name string `json:"name"`
FullName string `json:"full_name"`
Language string `json:"language"`
Private bool `json:"private"`
HTMLURL string `json:"html_url"`
CloneURL string `json:"clone_url"`
DefaultBranch string `json:"default_branch"`
} `json:"repository"`
// deployment hook details
Deployment struct {
ID int64 `json:"id"`
Sha string `json:"sha"`
Ref string `json:"ref"`
Task string `json:"task"`
Env string `json:"environment"`
URL string `json:"url"`
Desc string `json:"description"`
} `json:"deployment"`
// pull request details
PullRequest struct {
Number int `json:"number"`
State string `json:"state"`
Title string `json:"title"`
HTMLURL string `json:"html_url"`
User struct {
Login string `json:"login"`
Avatar string `json:"avatar_url"`
} `json:"user"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
Head struct {
SHA string `json:"sha"`
Ref string `json:"ref"`
Repo struct {
CloneURL string `json:"clone_url"`
} `json:"repo"`
} `json:"head"`
} `json:"pull_request"`
}

View file

@ -24,6 +24,11 @@ import (
"github.com/xanzy/go-gitlab" "github.com/xanzy/go-gitlab"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/shared/utils"
)
const (
mergeRefs = "refs/merge-requests/%d/head" // merge request merged with base
) )
func (g *Gitlab) convertGitlabRepo(_repo *gitlab.Project) (*model.Repo, error) { func (g *Gitlab) convertGitlabRepo(_repo *gitlab.Project) (*model.Repo, error) {
@ -59,7 +64,7 @@ func (g *Gitlab) convertGitlabRepo(_repo *gitlab.Project) (*model.Repo, error) {
return repo, nil return repo, nil
} }
func convertMergeRequestHock(hook *gitlab.MergeEvent, req *http.Request) (*model.Repo, *model.Build, error) { func convertMergeRequestHook(hook *gitlab.MergeEvent, req *http.Request) (int, *model.Repo, *model.Build, error) {
repo := &model.Repo{} repo := &model.Repo{}
build := &model.Build{} build := &model.Build{}
@ -68,17 +73,17 @@ func convertMergeRequestHock(hook *gitlab.MergeEvent, req *http.Request) (*model
obj := hook.ObjectAttributes obj := hook.ObjectAttributes
if target == nil && source == nil { if target == nil && source == nil {
return nil, nil, fmt.Errorf("target and source keys expected in merge request hook") return 0, nil, nil, fmt.Errorf("target and source keys expected in merge request hook")
} else if target == nil { } else if target == nil {
return nil, nil, fmt.Errorf("target key expected in merge request hook") return 0, nil, nil, fmt.Errorf("target key expected in merge request hook")
} else if source == nil { } else if source == nil {
return nil, nil, fmt.Errorf("source key expected in merge request hook") return 0, nil, nil, fmt.Errorf("source key expected in merge request hook")
} }
if target.PathWithNamespace != "" { if target.PathWithNamespace != "" {
var err error var err error
if repo.Owner, repo.Name, err = extractFromPath(target.PathWithNamespace); err != nil { if repo.Owner, repo.Name, err = extractFromPath(target.PathWithNamespace); err != nil {
return nil, nil, err return 0, nil, nil, err
} }
repo.FullName = target.PathWithNamespace repo.FullName = target.PathWithNamespace
} else { } else {
@ -113,8 +118,7 @@ func convertMergeRequestHock(hook *gitlab.MergeEvent, req *http.Request) (*model
build.Commit = lastCommit.ID build.Commit = lastCommit.ID
build.Remote = obj.Source.HTTPURL build.Remote = obj.Source.HTTPURL
build.Ref = fmt.Sprintf("refs/merge-requests/%d/head", obj.IID) build.Ref = fmt.Sprintf(mergeRefs, obj.IID)
build.Branch = obj.SourceBranch build.Branch = obj.SourceBranch
author := lastCommit.Author author := lastCommit.Author
@ -129,10 +133,10 @@ func convertMergeRequestHock(hook *gitlab.MergeEvent, req *http.Request) (*model
build.Title = obj.Title build.Title = obj.Title
build.Link = obj.URL build.Link = obj.URL
return repo, build, nil return obj.IID, repo, build, nil
} }
func convertPushHock(hook *gitlab.PushEvent) (*model.Repo, *model.Build, error) { func convertPushHook(hook *gitlab.PushEvent) (*model.Repo, *model.Build, error) {
repo := &model.Repo{} repo := &model.Repo{}
build := &model.Build{} build := &model.Build{}
@ -161,6 +165,8 @@ func convertPushHock(hook *gitlab.PushEvent) (*model.Repo, *model.Build, error)
build.Branch = strings.TrimPrefix(hook.Ref, "refs/heads/") build.Branch = strings.TrimPrefix(hook.Ref, "refs/heads/")
build.Ref = hook.Ref build.Ref = hook.Ref
// assume a capacity of 4 changed files per commit
files := make([]string, 0, len(hook.Commits)*4)
for _, cm := range hook.Commits { for _, cm := range hook.Commits {
if hook.After == cm.ID { if hook.After == cm.ID {
build.Author = cm.Author.Name build.Author = cm.Author.Name
@ -170,14 +176,18 @@ func convertPushHock(hook *gitlab.PushEvent) (*model.Repo, *model.Build, error)
if len(build.Email) != 0 { if len(build.Email) != 0 {
build.Avatar = getUserAvatar(build.Email) build.Avatar = getUserAvatar(build.Email)
} }
break
} }
files = append(files, cm.Added...)
files = append(files, cm.Removed...)
files = append(files, cm.Modified...)
} }
build.ChangedFiles = utils.DedupStrings(files)
return repo, build, nil return repo, build, nil
} }
func convertTagHock(hook *gitlab.TagEvent) (*model.Repo, *model.Build, error) { func convertTagHook(hook *gitlab.TagEvent) (*model.Repo, *model.Build, error) {
repo := &model.Repo{} repo := &model.Repo{}
build := &model.Build{} build := &model.Build{}

View file

@ -31,7 +31,9 @@ import (
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/common" "github.com/woodpecker-ci/woodpecker/server/remote/common"
"github.com/woodpecker-ci/woodpecker/server/store"
"github.com/woodpecker-ci/woodpecker/shared/oauth2" "github.com/woodpecker-ci/woodpecker/shared/oauth2"
"github.com/woodpecker-ci/woodpecker/shared/utils"
) )
const ( const (
@ -524,7 +526,7 @@ func (g *Gitlab) Branches(ctx context.Context, user *model.User, repo *model.Rep
// Hook parses the post-commit hook from the Request body // Hook parses the post-commit hook from the Request body
// and returns the required data in a standard format. // and returns the required data in a standard format.
func (g *Gitlab) Hook(req *http.Request) (*model.Repo, *model.Build, error) { func (g *Gitlab) Hook(ctx context.Context, req *http.Request) (*model.Repo, *model.Build, error) {
defer req.Body.Close() defer req.Body.Close()
payload, err := ioutil.ReadAll(req.Body) payload, err := ioutil.ReadAll(req.Body)
if err != nil { if err != nil {
@ -538,12 +540,62 @@ func (g *Gitlab) Hook(req *http.Request) (*model.Repo, *model.Build, error) {
switch event := parsed.(type) { switch event := parsed.(type) {
case *gitlab.MergeEvent: case *gitlab.MergeEvent:
return convertMergeRequestHock(event, req) mergeIID, repo, build, err := convertMergeRequestHook(event, req)
if err != nil {
return nil, nil, err
}
if build, err = g.loadChangedFilesFromMergeRequest(ctx, repo, build, mergeIID); err != nil {
return nil, nil, err
}
return repo, build, nil
case *gitlab.PushEvent: case *gitlab.PushEvent:
return convertPushHock(event) return convertPushHook(event)
case *gitlab.TagEvent: case *gitlab.TagEvent:
return convertTagHock(event) return convertTagHook(event)
default: default:
return nil, nil, nil return nil, nil, nil
} }
} }
func (g *Gitlab) loadChangedFilesFromMergeRequest(ctx context.Context, tmpRepo *model.Repo, build *model.Build, mergeIID int) (*model.Build, error) {
_store, ok := store.TryFromContext(ctx)
if !ok {
log.Error().Msg("could not get store from context")
return build, nil
}
repo, err := _store.GetRepoName(tmpRepo.Owner + "/" + tmpRepo.Name)
if err != nil {
return nil, err
}
user, err := _store.GetUser(repo.UserID)
if err != nil {
return nil, err
}
client, err := newClient(g.URL, user.Token, g.SkipVerify)
if err != nil {
return nil, err
}
_repo, err := g.getProject(ctx, client, repo.Owner, repo.Name)
if err != nil {
return nil, err
}
changes, _, err := client.MergeRequests.GetMergeRequestChanges(_repo.ID, mergeIID, &gitlab.GetMergeRequestChangesOptions{}, gitlab.WithContext(ctx))
if err != nil {
return nil, err
}
files := make([]string, 0, len(changes.Changes)*2)
for _, file := range changes.Changes {
files = append(files, file.NewPath, file.OldPath)
}
build.ChangedFiles = utils.DedupStrings(files)
return build, nil
}

View file

@ -51,7 +51,7 @@ func load(t *testing.T, config string) *Gitlab {
} }
func Test_Gitlab(t *testing.T) { func Test_Gitlab(t *testing.T) {
// setup a dummy github server // setup a dummy gitlab server
server := testdata.NewServer(t) server := testdata.NewServer(t)
defer server.Close() defer server.Close()
@ -169,7 +169,7 @@ func Test_Gitlab(t *testing.T) {
) )
req.Header = testdata.ServiceHookHeaders req.Header = testdata.ServiceHookHeaders
hookRepo, build, err := client.Hook(req) hookRepo, build, err := client.Hook(ctx, req)
assert.NoError(t, err) assert.NoError(t, err)
if assert.NotNil(t, hookRepo) && assert.NotNil(t, build) { if assert.NotNil(t, hookRepo) && assert.NotNil(t, build) {
assert.Equal(t, build.Event, model.EventPush) assert.Equal(t, build.Event, model.EventPush)
@ -178,6 +178,7 @@ func Test_Gitlab(t *testing.T) {
assert.Equal(t, "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", hookRepo.Avatar) assert.Equal(t, "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", hookRepo.Avatar)
assert.Equal(t, "develop", hookRepo.Branch) assert.Equal(t, "develop", hookRepo.Branch)
assert.Equal(t, "refs/heads/master", build.Ref) assert.Equal(t, "refs/heads/master", build.Ref)
assert.Equal(t, []string{"cmd/cli/main.go"}, build.ChangedFiles)
} }
}) })
}) })
@ -191,7 +192,7 @@ func Test_Gitlab(t *testing.T) {
) )
req.Header = testdata.ServiceHookHeaders req.Header = testdata.ServiceHookHeaders
hookRepo, build, err := client.Hook(req) hookRepo, build, err := client.Hook(ctx, req)
assert.NoError(t, err) assert.NoError(t, err)
if assert.NotNil(t, hookRepo) && assert.NotNil(t, build) { if assert.NotNil(t, hookRepo) && assert.NotNil(t, build) {
assert.Equal(t, "test", hookRepo.Owner) assert.Equal(t, "test", hookRepo.Owner)
@ -199,6 +200,7 @@ func Test_Gitlab(t *testing.T) {
assert.Equal(t, "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", hookRepo.Avatar) assert.Equal(t, "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", hookRepo.Avatar)
assert.Equal(t, "develop", hookRepo.Branch) assert.Equal(t, "develop", hookRepo.Branch)
assert.Equal(t, "refs/tags/v22", build.Ref) assert.Equal(t, "refs/tags/v22", build.Ref)
assert.Len(t, build.ChangedFiles, 0)
} }
}) })
}) })
@ -208,18 +210,20 @@ func Test_Gitlab(t *testing.T) {
req, _ := http.NewRequest( req, _ := http.NewRequest(
testdata.ServiceHookMethod, testdata.ServiceHookMethod,
testdata.ServiceHookURL.String(), testdata.ServiceHookURL.String(),
bytes.NewReader(testdata.ServiceHookMergeRequestBody), bytes.NewReader(testdata.WebhookMergeRequestBody),
) )
req.Header = testdata.ServiceHookHeaders req.Header = testdata.ServiceHookHeaders
hookRepo, build, err := client.Hook(req) // TODO: insert fake store into context to retrieve user & repo, this will activate fetching of ChangedFiles
hookRepo, build, err := client.Hook(ctx, req)
assert.NoError(t, err) assert.NoError(t, err)
if assert.NotNil(t, hookRepo) && assert.NotNil(t, build) { if assert.NotNil(t, hookRepo) && assert.NotNil(t, build) {
assert.Equal(t, "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", hookRepo.Avatar) assert.Equal(t, "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", hookRepo.Avatar)
assert.Equal(t, "develop", hookRepo.Branch) assert.Equal(t, "main", hookRepo.Branch)
assert.Equal(t, "test", hookRepo.Owner) assert.Equal(t, "anbraten", hookRepo.Owner)
assert.Equal(t, "woodpecker", hookRepo.Name) assert.Equal(t, "woodpecker", hookRepo.Name)
assert.Equal(t, "Update client.go 🎉", build.Title) assert.Equal(t, "Update client.go 🎉", build.Title)
assert.Len(t, build.ChangedFiles, 0) // see L217
} }
}) })
}) })

View file

@ -169,45 +169,45 @@ var ServiceHookTagPushBody = []byte(`{
} }
}`) }`)
// ServiceHookMergeRequestBody is payload of ServiceHook: MergeRequest // WebhookMergeRequestBody is payload of MergeEvent
var ServiceHookMergeRequestBody = []byte(`{ var WebhookMergeRequestBody = []byte(`{
"object_kind": "merge_request", "object_kind": "merge_request",
"event_type": "merge_request", "event_type": "merge_request",
"user": { "user": {
"id": 2, "id": 2251488,
"name": "the test", "name": "Anbraten",
"username": "test", "username": "anbraten",
"avatar_url": "https://www.gravatar.com/avatar/dd46a756faad4727fb679320751f6dea?s=80&d=identicon", "avatar_url": "https://secure.gravatar.com/avatar/fc9b6fe77c6b732a02925a62a81f05a0?s=80&d=identicon",
"email": "test@test.test" "email": "some@mail.info"
}, },
"project": { "project": {
"id": 2, "id": 32059612,
"name": "Woodpecker", "name": "woodpecker",
"description": "", "description": "",
"web_url": "http://10.40.8.5:3200/test/woodpecker", "web_url": "https://gitlab.com/anbraten/woodpecker",
"avatar_url": null, "avatar_url": "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg",
"git_ssh_url": "git@10.40.8.5:test/woodpecker.git", "git_ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"git_http_url": "http://10.40.8.5:3200/test/woodpecker.git", "git_http_url": "https://gitlab.com/anbraten/woodpecker.git",
"namespace": "the test", "namespace": "Anbraten",
"visibility_level": 20, "visibility_level": 20,
"path_with_namespace": "test/woodpecker", "path_with_namespace": "anbraten/woodpecker",
"default_branch": "master", "default_branch": "main",
"ci_config_path": null, "ci_config_path": "",
"homepage": "http://10.40.8.5:3200/test/woodpecker", "homepage": "https://gitlab.com/anbraten/woodpecker",
"url": "git@10.40.8.5:test/woodpecker.git", "url": "git@gitlab.com:anbraten/woodpecker.git",
"ssh_url": "git@10.40.8.5:test/woodpecker.git", "ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"http_url": "http://10.40.8.5:3200/test/woodpecker.git" "http_url": "https://gitlab.com/anbraten/woodpecker.git"
}, },
"object_attributes": { "object_attributes": {
"assignee_id": null, "assignee_id": 2251488,
"author_id": 2, "author_id": 2251488,
"created_at": "2021-09-27 05:00:01 UTC", "created_at": "2022-01-10 15:23:41 UTC",
"description": "", "description": "",
"head_pipeline_id": 5, "head_pipeline_id": 449733536,
"id": 2, "id": 134400602,
"iid": 2, "iid": 3,
"last_edited_at": null, "last_edited_at": "2022-01-17 15:46:23 UTC",
"last_edited_by_id": null, "last_edited_by_id": 2251488,
"merge_commit_sha": null, "merge_commit_sha": null,
"merge_error": null, "merge_error": null,
"merge_params": { "merge_params": {
@ -217,61 +217,61 @@ var ServiceHookMergeRequestBody = []byte(`{
"merge_user_id": null, "merge_user_id": null,
"merge_when_pipeline_succeeds": false, "merge_when_pipeline_succeeds": false,
"milestone_id": null, "milestone_id": null,
"source_branch": "masterfdsafds", "source_branch": "anbraten-main-patch-05373",
"source_project_id": 2, "source_project_id": 32059612,
"state_id": 1, "state_id": 1,
"target_branch": "master", "target_branch": "main",
"target_project_id": 2, "target_project_id": 32059612,
"time_estimate": 0, "time_estimate": 0,
"title": "Update client.go 🎉", "title": "Update client.go 🎉",
"updated_at": "2021-09-27 05:01:21 UTC", "updated_at": "2022-01-17 15:47:39 UTC",
"updated_by_id": null, "updated_by_id": 2251488,
"url": "http://10.40.8.5:3200/test/woodpecker/-/merge_requests/2", "url": "https://gitlab.com/anbraten/woodpecker/-/merge_requests/3",
"source": { "source": {
"id": 2, "id": 32059612,
"name": "Woodpecker", "name": "woodpecker",
"description": "", "description": "",
"web_url": "http://10.40.8.5:3200/test/woodpecker", "web_url": "https://gitlab.com/anbraten/woodpecker",
"avatar_url": "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", "avatar_url": null,
"git_ssh_url": "git@10.40.8.5:test/woodpecker.git", "git_ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"git_http_url": "http://10.40.8.5:3200/test/woodpecker.git", "git_http_url": "https://gitlab.com/anbraten/woodpecker.git",
"namespace": "the test", "namespace": "Anbraten",
"visibility_level": 20, "visibility_level": 20,
"path_with_namespace": "test/woodpecker", "path_with_namespace": "anbraten/woodpecker",
"default_branch": "develop", "default_branch": "main",
"ci_config_path": null, "ci_config_path": "",
"homepage": "http://10.40.8.5:3200/test/woodpecker", "homepage": "https://gitlab.com/anbraten/woodpecker",
"url": "git@10.40.8.5:test/woodpecker.git", "url": "git@gitlab.com:anbraten/woodpecker.git",
"ssh_url": "git@10.40.8.5:test/woodpecker.git", "ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"http_url": "http://10.40.8.5:3200/test/woodpecker.git" "http_url": "https://gitlab.com/anbraten/woodpecker.git"
}, },
"target": { "target": {
"id": 2, "id": 32059612,
"name": "Woodpecker", "name": "woodpecker",
"description": "", "description": "",
"web_url": "http://10.40.8.5:3200/test/woodpecker", "web_url": "https://gitlab.com/anbraten/woodpecker",
"avatar_url": "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg", "avatar_url": "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg",
"git_ssh_url": "git@10.40.8.5:test/woodpecker.git", "git_ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"git_http_url": "http://10.40.8.5:3200/test/woodpecker.git", "git_http_url": "https://gitlab.com/anbraten/woodpecker.git",
"namespace": "the test", "namespace": "Anbraten",
"visibility_level": 20, "visibility_level": 20,
"path_with_namespace": "test/woodpecker", "path_with_namespace": "anbraten/woodpecker",
"default_branch": "develop", "default_branch": "main",
"ci_config_path": null, "ci_config_path": "",
"homepage": "http://10.40.8.5:3200/test/woodpecker", "homepage": "https://gitlab.com/anbraten/woodpecker",
"url": "git@10.40.8.5:test/woodpecker.git", "url": "git@gitlab.com:anbraten/woodpecker.git",
"ssh_url": "git@10.40.8.5:test/woodpecker.git", "ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"http_url": "http://10.40.8.5:3200/test/woodpecker.git" "http_url": "https://gitlab.com/anbraten/woodpecker.git"
}, },
"last_commit": { "last_commit": {
"id": "0ab96a10266b95b4b533dcfd98738015fbe70889", "id": "c136499ec574e1034b24c5d306de9acda3005367",
"message": "Update state.go", "message": "Update folder/todo.txt",
"title": "Update state.go", "title": "Update folder/todo.txt",
"timestamp": "2021-09-27T05:01:20+00:00", "timestamp": "2022-01-17T15:47:38+00:00",
"url": "http://10.40.8.5:3200/test/woodpecker/-/commit/0ab96a10266b95b4b533dcfd98738015fbe70889", "url": "https://gitlab.com/anbraten/woodpecker/-/commit/c136499ec574e1034b24c5d306de9acda3005367",
"author": { "author": {
"name": "the test", "name": "Anbraten",
"email": "test@test.test" "email": "some@mail.info"
} }
}, },
"work_in_progress": false, "work_in_progress": false,
@ -281,25 +281,36 @@ var ServiceHookMergeRequestBody = []byte(`{
"human_time_change": null, "human_time_change": null,
"human_time_estimate": null, "human_time_estimate": null,
"assignee_ids": [ "assignee_ids": [
2251488
], ],
"state": "opened", "state": "opened",
"blocking_discussions_resolved": true,
"action": "update", "action": "update",
"oldrev": "6ef047571374c96a2bf13c361efd1fb008b0063e" "oldrev": "8b641937b7340066d882b9d8a8cc5b0573a207de"
}, },
"labels": [ "labels": [
], ],
"changes": { "changes": {
"updated_at": { "updated_at": {
"previous": "2021-09-27 05:00:01 UTC", "previous": "2022-01-17 15:46:23 UTC",
"current": "2021-09-27 05:01:21 UTC" "current": "2022-01-17 15:47:39 UTC"
} }
}, },
"repository": { "repository": {
"name": "Woodpecker", "name": "woodpecker",
"url": "git@10.40.8.5:test/woodpecker.git", "url": "git@gitlab.com:anbraten/woodpecker.git",
"description": "", "description": "",
"homepage": "http://10.40.8.5:3200/test/woodpecker" "homepage": "https://gitlab.com/anbraten/woodpecker"
} },
}`) "assignees": [
{
"id": 2251488,
"name": "Anbraten",
"username": "anbraten",
"avatar_url": "https://secure.gravatar.com/avatar/fc9b6fe77c6b732a02925a62a81f05a0?s=80&d=identicon",
"email": "some@mail.info"
}
]
}
`)

View file

@ -264,7 +264,7 @@ func (c *client) Branches(ctx context.Context, u *model.User, r *model.Repo) ([]
// Hook parses the incoming Gogs hook and returns the Repository and Build // Hook parses the incoming Gogs hook and returns the Repository and Build
// details. If the hook is unsupported nil values are returned. // details. If the hook is unsupported nil values are returned.
func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) { func (c *client) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error) {
return parseHook(r) return parseHook(r)
} }

View file

@ -136,13 +136,13 @@ func (_m *Remote) File(ctx context.Context, u *model.User, r *model.Repo, b *mod
return r0, r1 return r0, r1
} }
// Hook provides a mock function with given fields: r // Hook provides a mock function with given fields: ctx, r
func (_m *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) { func (_m *Remote) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error) {
ret := _m.Called(r) ret := _m.Called(ctx, r)
var r0 *model.Repo var r0 *model.Repo
if rf, ok := ret.Get(0).(func(*http.Request) *model.Repo); ok { if rf, ok := ret.Get(0).(func(context.Context, *http.Request) *model.Repo); ok {
r0 = rf(r) r0 = rf(ctx, r)
} else { } else {
if ret.Get(0) != nil { if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Repo) r0 = ret.Get(0).(*model.Repo)
@ -150,8 +150,8 @@ func (_m *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
} }
var r1 *model.Build var r1 *model.Build
if rf, ok := ret.Get(1).(func(*http.Request) *model.Build); ok { if rf, ok := ret.Get(1).(func(context.Context, *http.Request) *model.Build); ok {
r1 = rf(r) r1 = rf(ctx, r)
} else { } else {
if ret.Get(1) != nil { if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.Build) r1 = ret.Get(1).(*model.Build)
@ -159,8 +159,8 @@ func (_m *Remote) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
} }
var r2 error var r2 error
if rf, ok := ret.Get(2).(func(*http.Request) error); ok { if rf, ok := ret.Get(2).(func(context.Context, *http.Request) error); ok {
r2 = rf(r) r2 = rf(ctx, r)
} else { } else {
r2 = ret.Error(2) r2 = ret.Error(2)
} }

View file

@ -75,7 +75,7 @@ type Remote interface {
// Hook parses the post-commit hook from the Request body and returns the // Hook parses the post-commit hook from the Request body and returns the
// required data in a standard format. // required data in a standard format.
Hook(r *http.Request) (*model.Repo, *model.Build, error) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error)
} }
// FileMeta represents a file in version control // FileMeta represents a file in version control

View file

@ -30,6 +30,12 @@ func FromContext(c context.Context) Store {
return c.Value(key).(Store) return c.Value(key).(Store)
} }
// TryFromContext try to return the Store associated with this context.
func TryFromContext(c context.Context) (Store, bool) {
store, ok := c.Value(key).(Store)
return store, ok
}
// ToContext adds the Store to this context if it supports // ToContext adds the Store to this context if it supports
// the Setter interface. // the Setter interface.
func ToContext(c Setter, store Store) { func ToContext(c Setter, store Store) {

32
shared/utils/strings.go Normal file
View file

@ -0,0 +1,32 @@
// Copyright 2022 Woodpecker Authors
//
// 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 utils
// DedupStrings deduplicate string list, empty items are dropped
func DedupStrings(list []string) []string {
m := make(map[string]struct{}, len(list))
for i := range list {
if s := list[i]; len(s) > 0 {
m[list[i]] = struct{}{}
}
}
newList := make([]string, 0, len(m))
for k := range m {
newList = append(newList, k)
}
return newList
}

View file

@ -0,0 +1,48 @@
// Copyright 2022 Woodpecker Authors
//
// 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 utils
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDedupStrings(t *testing.T) {
tests := []struct {
in []string
out []string
}{{
in: []string{"", "ab", "12", "ab"},
out: []string{"12", "ab"},
}, {
in: nil,
out: nil,
}, {
in: []string{""},
out: nil,
}}
for _, tc := range tests {
result := DedupStrings(tc.in)
sort.Strings(result)
if len(tc.out) == 0 {
assert.Len(t, result, 0)
} else {
assert.EqualValues(t, tc.out, result, "could not correctly process input '%#v'", tc.in)
}
}
}