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
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.
:::

View file

@ -78,7 +78,7 @@ func BlockTilQueueHasRunningItem(c *gin.Context) {
func PostHook(c *gin.Context) {
_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 {
msg := "failure to parse hook"
log.Debug().Err(err).Msg(msg)
@ -288,6 +288,7 @@ func branchFiltered(build *model.Build, remoteYamlConfigs []*remote.FileMeta) (b
return false, 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
// 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)
}

View file

@ -264,7 +264,7 @@ func Test_bitbucket(t *testing.T) {
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush)
r, _, err := c.Hook(req)
r, _, err := c.Hook(ctx, req)
g.Assert(err).IsNil()
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)
}
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)
}

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
// 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)
if build != nil {
build.Avatar = c.resourceLink(build.Avatar)

View file

@ -223,7 +223,7 @@ func Test_coding(t *testing.T) {
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush)
r, _, err := c.Hook(req)
r, _, err := c.Hook(ctx, req)
g.Assert(err).IsNil()
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
// 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)
}

View file

@ -25,6 +25,7 @@ import (
"code.gitea.io/sdk/gitea"
"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.
@ -110,15 +111,15 @@ func buildFromPush(hook *pushHook) *model.Build {
}
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 {
files = append(files, c.Added...)
files = append(files, c.Removed...)
files = append(files, c.Modified...)
}
return files
return utils.DedupStrings(files)
}
// helper function that extracts the Build data from a Gitea tag hook

View file

@ -15,9 +15,6 @@
package github
import (
"fmt"
"strings"
"github.com/google/go-github/v39/github"
"github.com/woodpecker-ci/woodpecker/server/model"
@ -44,7 +41,7 @@ const (
const (
headRefs = "refs/pull/%d/head" // pull request unmerged
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
@ -85,19 +82,19 @@ func convertDesc(status model.StatusValue) string {
// structure to the common Woodpecker repository structure.
func convertRepo(from *github.Repository, private bool) *model.Repo {
repo := &model.Repo{
Owner: *from.Owner.Login,
Name: *from.Name,
FullName: *from.FullName,
Link: *from.HTMLURL,
IsSCMPrivate: *from.Private,
Clone: *from.CloneURL,
Avatar: *from.Owner.AvatarURL,
Name: from.GetName(),
FullName: from.GetFullName(),
Link: from.GetHTMLURL(),
IsSCMPrivate: from.GetPrivate(),
Clone: from.GetCloneURL(),
Branch: from.GetDefaultBranch(),
Owner: from.GetOwner().GetLogin(),
Avatar: from.GetOwner().GetAvatarURL(),
Perm: convertPerm(from.GetPermissions()),
SCMKind: model.RepoGit,
Branch: defaultBranch,
Perm: convertPerm(from),
}
if from.DefaultBranch != nil {
repo.Branch = *from.DefaultBranch
if len(repo.Branch) == 0 {
repo.Branch = defaultBranch
}
if private {
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
// 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{
Admin: from.Permissions["admin"],
Push: from.Permissions["push"],
Pull: from.Permissions["pull"],
Admin: perm["admin"],
Push: perm["push"],
Pull: perm["pull"],
}
}
@ -139,143 +136,29 @@ func convertTeamList(from []*github.Organization) []*model.Team {
// to the common Woodpecker repository structure.
func convertTeam(from *github.Organization) *model.Team {
return &model.Team{
Login: *from.Login,
Avatar: *from.AvatarURL,
Login: from.GetLogin(),
Avatar: from.GetAvatarURL(),
}
}
// convertRepoHook is a helper function used to extract the Repository details
// 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{
Owner: from.Repo.Owner.Login,
Name: from.Repo.Name,
FullName: from.Repo.FullName,
Link: from.Repo.HTMLURL,
IsSCMPrivate: from.Repo.Private,
Clone: from.Repo.CloneURL,
Branch: from.Repo.DefaultBranch,
Owner: eventRepo.GetOwner().GetLogin(),
Name: eventRepo.GetName(),
FullName: eventRepo.GetFullName(),
Link: eventRepo.GetHTMLURL(),
IsSCMPrivate: eventRepo.GetPrivate(),
Clone: eventRepo.GetCloneURL(),
Branch: eventRepo.GetDefaultBranch(),
SCMKind: model.RepoGit,
}
if repo.Branch == "" {
repo.Branch = defaultBranch
}
if repo.Owner == "" { // legacy webhooks
repo.Owner = from.Repo.Owner.Name
}
if repo.FullName == "" {
repo.FullName = repo.Owner + "/" + repo.Name
}
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.Pull).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() {
from := &webhook{}
from.Repo.Owner.Login = "octocat"
from.Repo.Owner.Name = "octocat"
from.Repo.Name = "hello-world"
from.Repo.FullName = "octocat/hello-world"
from.Repo.Private = true
from.Repo.HTMLURL = "https://github.com/octocat/hello-world"
from.Repo.CloneURL = "https://github.com/octocat/hello-world.git"
from.Repo.DefaultBranch = "develop"
from := &github.PushEventRepository{Owner: &github.User{}}
from.Owner.Login = github.String("octocat")
from.Owner.Name = github.String("octocat")
from.Name = github.String("hello-world")
from.FullName = github.String("octocat/hello-world")
from.Private = github.Bool(true)
from.HTMLURL = github.String("https://github.com/octocat/hello-world")
from.CloneURL = github.String("https://github.com/octocat/hello-world.git")
from.DefaultBranch = github.String("develop")
repo := convertRepoHook(from)
g.Assert(repo.Owner).Equal(from.Repo.Owner.Login)
g.Assert(repo.Name).Equal(from.Repo.Name)
g.Assert(repo.FullName).Equal(from.Repo.FullName)
g.Assert(repo.IsSCMPrivate).Equal(from.Repo.Private)
g.Assert(repo.Link).Equal(from.Repo.HTMLURL)
g.Assert(repo.Clone).Equal(from.Repo.CloneURL)
g.Assert(repo.Branch).Equal(from.Repo.DefaultBranch)
g.Assert(repo.Owner).Equal(*from.Owner.Login)
g.Assert(repo.Name).Equal(*from.Name)
g.Assert(repo.FullName).Equal(*from.FullName)
g.Assert(repo.IsSCMPrivate).Equal(*from.Private)
g.Assert(repo.Link).Equal(*from.HTMLURL)
g.Assert(repo.Clone).Equal(*from.CloneURL)
g.Assert(repo.Branch).Equal(*from.DefaultBranch)
})
g.It("should convert a pull request from webhook", func() {
from := &webhook{}
from.PullRequest.Base.Ref = "master"
from.PullRequest.Head.Ref = "changes"
from.PullRequest.Head.SHA = "f72fc19"
from.PullRequest.Head.Repo.CloneURL = "https://github.com/octocat/hello-world-fork"
from.PullRequest.HTMLURL = "https://github.com/octocat/hello-world/pulls/42"
from.PullRequest.Number = 42
from.PullRequest.Title = "Updated README.md"
from.PullRequest.User.Login = "octocat"
from.PullRequest.User.Avatar = "https://avatars1.githubusercontent.com/u/583231"
from.Sender.Login = "octocat"
build := convertPullHook(from, true)
from := &github.PullRequestEvent{
Action: github.String(actionOpen),
PullRequest: &github.PullRequest{
State: github.String(stateOpen),
HTMLURL: github.String("https://github.com/octocat/hello-world/pulls/42"),
Number: github.Int(42),
Title: github.String("Updated README.md"),
Base: &github.PullRequestBranch{
Ref: github.String("master"),
},
Head: &github.PullRequestBranch{
Ref: github.String("changes"),
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.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.Refspec).Equal("changes:master")
g.Assert(build.Remote).Equal("https://github.com/octocat/hello-world-fork")
g.Assert(build.Commit).Equal(from.PullRequest.Head.SHA)
g.Assert(build.Message).Equal(from.PullRequest.Title)
g.Assert(build.Title).Equal(from.PullRequest.Title)
g.Assert(build.Author).Equal(from.PullRequest.User.Login)
g.Assert(build.Avatar).Equal(from.PullRequest.User.Avatar)
g.Assert(build.Sender).Equal(from.Sender.Login)
g.Assert(build.Commit).Equal(*from.PullRequest.Head.SHA)
g.Assert(build.Message).Equal(*from.PullRequest.Title)
g.Assert(build.Title).Equal(*from.PullRequest.Title)
g.Assert(build.Author).Equal(*from.PullRequest.User.Login)
g.Assert(build.Avatar).Equal(*from.PullRequest.User.AvatarURL)
g.Assert(build.Sender).Equal(*from.Sender.Login)
})
g.It("should convert a deployment from webhook", func() {
from := &webhook{}
from.Deployment.Desc = ":shipit:"
from.Deployment.Env = "production"
from.Deployment.ID = 42
from.Deployment.Ref = "master"
from.Deployment.Sha = "f72fc19"
from.Deployment.URL = "https://github.com/octocat/hello-world"
from.Sender.Login = "octocat"
from.Sender.Avatar = "https://avatars1.githubusercontent.com/u/583231"
from := &github.DeploymentEvent{Deployment: &github.Deployment{}, Sender: &github.User{}}
from.Deployment.Description = github.String(":shipit:")
from.Deployment.Environment = github.String("production")
from.Deployment.ID = github.Int64(42)
from.Deployment.Ref = github.String("master")
from.Deployment.SHA = github.String("f72fc19")
from.Deployment.URL = github.String("https://github.com/octocat/hello-world")
from.Sender.Login = github.String("octocat")
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.Branch).Equal("master")
g.Assert(build.Ref).Equal("refs/heads/master")
g.Assert(build.Commit).Equal(from.Deployment.Sha)
g.Assert(build.Message).Equal(from.Deployment.Desc)
g.Assert(build.Link).Equal(from.Deployment.URL)
g.Assert(build.Author).Equal(from.Sender.Login)
g.Assert(build.Avatar).Equal(from.Sender.Avatar)
g.Assert(build.Commit).Equal(*from.Deployment.SHA)
g.Assert(build.Message).Equal(*from.Deployment.Description)
g.Assert(build.Link).Equal(*from.Deployment.URL)
g.Assert(build.Author).Equal(*from.Sender.Login)
g.Assert(build.Avatar).Equal(*from.Sender.AvatarURL)
})
g.It("should convert a push from webhook", func() {
from := &webhook{}
from.Sender.Login = "octocat"
from.Sender.Avatar = "https://avatars1.githubusercontent.com/u/583231"
from.Repo.CloneURL = "https://github.com/octocat/hello-world.git"
from.Head.Author.Email = "octocat@github.com"
from.Head.Message = "updated README.md"
from.Head.URL = "https://github.com/octocat/hello-world"
from.Head.ID = "f72fc19"
from.Ref = "refs/heads/master"
from := &github.PushEvent{Sender: &github.User{}, Repo: &github.PushEventRepository{}, HeadCommit: &github.HeadCommit{Author: &github.CommitAuthor{}}}
from.Sender.Login = github.String("octocat")
from.Sender.AvatarURL = github.String("https://avatars1.githubusercontent.com/u/583231")
from.Repo.CloneURL = github.String("https://github.com/octocat/hello-world.git")
from.HeadCommit.Author.Email = github.String("github.String(octocat@github.com")
from.HeadCommit.Message = github.String("updated README.md")
from.HeadCommit.URL = github.String("https://github.com/octocat/hello-world")
from.HeadCommit.ID = github.String("f72fc19")
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.Branch).Equal("master")
g.Assert(build.Ref).Equal("refs/heads/master")
g.Assert(build.Commit).Equal(from.Head.ID)
g.Assert(build.Message).Equal(from.Head.Message)
g.Assert(build.Link).Equal(from.Head.URL)
g.Assert(build.Author).Equal(from.Sender.Login)
g.Assert(build.Avatar).Equal(from.Sender.Avatar)
g.Assert(build.Email).Equal(from.Head.Author.Email)
g.Assert(build.Remote).Equal(from.Repo.CloneURL)
g.Assert(build.Commit).Equal(*from.HeadCommit.ID)
g.Assert(build.Message).Equal(*from.HeadCommit.Message)
g.Assert(build.Link).Equal(*from.HeadCommit.URL)
g.Assert(build.Author).Equal(*from.Sender.Login)
g.Assert(build.Avatar).Equal(*from.Sender.AvatarURL)
g.Assert(build.Email).Equal(*from.HeadCommit.Author.Email)
g.Assert(build.Remote).Equal(*from.Repo.CloneURL)
})
g.It("should convert a tag from webhook", func() {
from := &webhook{}
from.Ref = "refs/tags/v1.0.0"
from := &github.PushEvent{}
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.Ref).Equal("refs/tags/v1.0.0")
})
g.It("should convert tag's base branch from webhook to build's branch ", func() {
from := &webhook{}
from.Ref = "refs/tags/v1.0.0"
from.BaseRef = "refs/heads/master"
from := &github.PushEvent{}
from.Ref = github.String("refs/tags/v1.0.0")
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.Branch).Equal("master")
})
g.It("should not convert tag's base_ref from webhook if not prefixed with 'ref/heads/'", func() {
from := &webhook{}
from.Ref = "refs/tags/v1.0.0"
from.BaseRef = "refs/refs/master"
from := &github.PushEvent{}
from.Ref = github.String("refs/tags/v1.0.0")
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.Branch).Equal("refs/tags/v1.0.0")
})

View file

@ -16,55 +16,230 @@ package fixtures
// HookPush is a sample push hook.
// https://developer.github.com/v3/activity/events/types/#pushevent
const HookPush = `
{
"ref": "refs/heads/changes",
"created": false,
"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"]
},
const HookPush = `{
"ref": "refs/heads/master",
"before": "2f780193b136b72bfea4eeb640786a8c4450c7a2",
"after": "366701fde727cb7a9e7f21eb88264f59f6f9b89c",
"repository": {
"id": 35129377,
"name": "public-repo",
"full_name": "baxterthehacker/public-repo",
"owner": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com"
},
"id": 179344069,
"node_id": "MDEwOlJlcG9zaXRvcnkxNzkzNDQwNjk=",
"name": "woodpecker",
"full_name": "woodpecker-ci/woodpecker",
"private": false,
"html_url": "https://github.com/baxterthehacker/public-repo",
"default_branch": "master"
"owner": {
"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": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com"
"name": "6543",
"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": {
"login": "baxterthehacker",
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3"
}
}
`
"login": "6543",
"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 = `
{
"deleted": true

View file

@ -0,0 +1 @@
package fixtures

View file

@ -26,12 +26,15 @@ import (
"strings"
"github.com/google/go-github/v39/github"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2"
"github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote"
"github.com/woodpecker-ci/woodpecker/server/remote/common"
"github.com/woodpecker-ci/woodpecker/server/store"
"github.com/woodpecker-ci/woodpecker/shared/utils"
)
const (
@ -219,7 +222,7 @@ func (c *client) Perm(ctx context.Context, u *model.User, r *model.Repo) (*model
if err != nil {
return nil, err
}
return convertPerm(repo), nil
return convertPerm(repo.GetPermissions()), nil
}
// 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
// and returns the required data in a standard format.
func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
return parseHook(r, c.MergeRef)
func (c *client) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Build, error) {
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 (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/google/go-github/v39/github"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/shared/utils"
)
const (
hookEvent = "X-Github-Event"
hookField = "payload"
hookDeploy = "deployment"
hookPush = "push"
hookPull = "pull_request"
actionOpen = "opened"
actionSync = "synchronize"
@ -39,7 +39,7 @@ const (
// 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.
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
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)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
switch r.Header.Get(hookEvent) {
case hookPush:
return parsePushHook(raw)
case hookDeploy:
return parseDeployHook(raw)
case hookPull:
return parsePullHook(raw, merge)
payload, err := github.ParseWebHook(github.WebHookType(r), raw)
if err != nil {
return nil, nil, nil, err
}
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.
// If the commit type is unsupported nil values are returned.
func parsePushHook(payload []byte) (*model.Repo, *model.Build, error) {
hook := new(webhook)
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, nil, err
func parsePushHook(hook *github.PushEvent) (*model.Repo, *model.Build, error) {
if hook.Deleted != nil && *hook.Deleted {
return nil, nil, nil
}
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.
// If the commit type is unsupported nil values are returned.
func parseDeployHook(payload []byte) (*model.Repo, *model.Build, error) {
hook := new(webhook)
if err := json.Unmarshal(payload, hook); err != nil {
return nil, nil, err
func parseDeployHook(hook *github.DeploymentEvent, privateMode bool) (*model.Repo, *model.Build, error) {
build := &model.Build{
Event: model.EventDeploy,
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
// details. If the pull request is closed nil values are returned.
func parsePullHook(payload []byte, merge bool) (*model.Repo, *model.Build, error) {
hook := new(webhook)
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, nil, err
func parsePullHook(hook *github.PullRequestEvent, merge, privateMode bool) (*github.PullRequest, *model.Repo, *model.Build, error) {
// only listen to new merge-requests and pushes to open ones
if hook.GetAction() != actionOpen && hook.GetAction() != actionSync {
return nil, nil, nil, nil
}
if hook.GetPullRequest().GetState() != stateOpen {
return nil, nil, nil, nil
}
// ignore these
if hook.Action != actionOpen && hook.Action != actionSync {
return nil, nil, nil
build := &model.Build{
Event: model.EventPull,
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 {
return nil, nil, nil
if merge {
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 (
"bytes"
"net/http"
"sort"
"testing"
"github.com/franela/goblin"
@ -25,85 +26,91 @@ import (
"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) {
g := goblin.Goblin(t)
g.Describe("GitHub parser", func() {
g.It("should ignore unsupported hook events", func() {
buf := bytes.NewBufferString(fixtures.HookPullRequest)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, "issues")
r, b, err := parseHook(req, false)
req := testHookRequest([]byte(fixtures.HookPullRequest), "issues")
p, r, b, err := parseHook(req, false, false)
g.Assert(r).IsNil()
g.Assert(b).IsNil()
g.Assert(err).IsNil()
g.Assert(p).IsNil()
})
g.Describe("given a push hook", func() {
g.It("should skip when action is deleted", func() {
raw := []byte(fixtures.HookPushDeleted)
r, b, err := parsePushHook(raw)
req := testHookRequest([]byte(fixtures.HookPushDeleted), hookPush)
p, r, b, err := parseHook(req, false, false)
g.Assert(r).IsNil()
g.Assert(b).IsNil()
g.Assert(err).IsNil()
g.Assert(p).IsNil()
})
g.It("should extract repository and build details", func() {
buf := bytes.NewBufferString(fixtures.HookPush)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPush)
r, b, err := parseHook(req, false)
req := testHookRequest([]byte(fixtures.HookPush), hookPush)
p, r, b, err := parseHook(req, false, false)
g.Assert(err).IsNil()
g.Assert(p).IsNil()
g.Assert(r).IsNotNil()
g.Assert(b).IsNotNil()
g.Assert(b.Event).Equal(model.EventPush)
expectedFiles := []string{"CHANGELOG.md", "app/controller/application.rb"}
g.Assert(b.ChangedFiles).Equal(expectedFiles)
sort.Strings(b.ChangedFiles)
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.It("should skip when action is not open or sync", func() {
raw := []byte(fixtures.HookPullRequestInvalidAction)
r, b, err := parsePullHook(raw, false)
req := testHookRequest([]byte(fixtures.HookPullRequestInvalidAction), hookPull)
p, r, b, err := parseHook(req, false, false)
g.Assert(r).IsNil()
g.Assert(b).IsNil()
g.Assert(err).IsNil()
g.Assert(p).IsNil()
})
g.It("should skip when state is not open", func() {
raw := []byte(fixtures.HookPullRequestInvalidState)
r, b, err := parsePullHook(raw, false)
req := testHookRequest([]byte(fixtures.HookPullRequestInvalidState), hookPull)
p, r, b, err := parseHook(req, false, false)
g.Assert(r).IsNil()
g.Assert(b).IsNil()
g.Assert(err).IsNil()
g.Assert(p).IsNil()
})
g.It("should extract repository and build details", func() {
buf := bytes.NewBufferString(fixtures.HookPullRequest)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPull)
r, b, err := parseHook(req, false)
req := testHookRequest([]byte(fixtures.HookPullRequest), hookPull)
p, r, b, err := parseHook(req, false, false)
g.Assert(err).IsNil()
g.Assert(r).IsNotNil()
g.Assert(b).IsNotNil()
g.Assert(p).IsNotNil()
g.Assert(b.Event).Equal(model.EventPull)
})
})
g.Describe("given a deployment hook", func() {
g.It("should extract repository and build details", func() {
buf := bytes.NewBufferString(fixtures.HookDeploy)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookDeploy)
r, b, err := parseHook(req, false)
req := testHookRequest([]byte(fixtures.HookDeploy), hookDeploy)
p, r, b, err := parseHook(req, false, false)
g.Assert(err).IsNil()
g.Assert(r).IsNotNil()
g.Assert(b).IsNotNil()
g.Assert(p).IsNil()
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/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) {
@ -59,7 +64,7 @@ func (g *Gitlab) convertGitlabRepo(_repo *gitlab.Project) (*model.Repo, error) {
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{}
build := &model.Build{}
@ -68,17 +73,17 @@ func convertMergeRequestHock(hook *gitlab.MergeEvent, req *http.Request) (*model
obj := hook.ObjectAttributes
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 {
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 {
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 != "" {
var err error
if repo.Owner, repo.Name, err = extractFromPath(target.PathWithNamespace); err != nil {
return nil, nil, err
return 0, nil, nil, err
}
repo.FullName = target.PathWithNamespace
} else {
@ -113,8 +118,7 @@ func convertMergeRequestHock(hook *gitlab.MergeEvent, req *http.Request) (*model
build.Commit = lastCommit.ID
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
author := lastCommit.Author
@ -129,10 +133,10 @@ func convertMergeRequestHock(hook *gitlab.MergeEvent, req *http.Request) (*model
build.Title = obj.Title
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{}
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.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 {
if hook.After == cm.ID {
build.Author = cm.Author.Name
@ -170,14 +176,18 @@ func convertPushHock(hook *gitlab.PushEvent) (*model.Repo, *model.Build, error)
if len(build.Email) != 0 {
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
}
func convertTagHock(hook *gitlab.TagEvent) (*model.Repo, *model.Build, error) {
func convertTagHook(hook *gitlab.TagEvent) (*model.Repo, *model.Build, error) {
repo := &model.Repo{}
build := &model.Build{}

View file

@ -31,7 +31,9 @@ import (
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote"
"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/utils"
)
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
// 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()
payload, err := ioutil.ReadAll(req.Body)
if err != nil {
@ -538,12 +540,62 @@ func (g *Gitlab) Hook(req *http.Request) (*model.Repo, *model.Build, error) {
switch event := parsed.(type) {
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:
return convertPushHock(event)
return convertPushHook(event)
case *gitlab.TagEvent:
return convertTagHock(event)
return convertTagHook(event)
default:
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) {
// setup a dummy github server
// setup a dummy gitlab server
server := testdata.NewServer(t)
defer server.Close()
@ -169,7 +169,7 @@ func Test_Gitlab(t *testing.T) {
)
req.Header = testdata.ServiceHookHeaders
hookRepo, build, err := client.Hook(req)
hookRepo, build, err := client.Hook(ctx, req)
assert.NoError(t, err)
if assert.NotNil(t, hookRepo) && assert.NotNil(t, build) {
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, "develop", hookRepo.Branch)
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
hookRepo, build, err := client.Hook(req)
hookRepo, build, err := client.Hook(ctx, req)
assert.NoError(t, err)
if assert.NotNil(t, hookRepo) && assert.NotNil(t, build) {
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, "develop", hookRepo.Branch)
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(
testdata.ServiceHookMethod,
testdata.ServiceHookURL.String(),
bytes.NewReader(testdata.ServiceHookMergeRequestBody),
bytes.NewReader(testdata.WebhookMergeRequestBody),
)
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)
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, "develop", hookRepo.Branch)
assert.Equal(t, "test", hookRepo.Owner)
assert.Equal(t, "main", hookRepo.Branch)
assert.Equal(t, "anbraten", hookRepo.Owner)
assert.Equal(t, "woodpecker", hookRepo.Name)
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
var ServiceHookMergeRequestBody = []byte(`{
// WebhookMergeRequestBody is payload of MergeEvent
var WebhookMergeRequestBody = []byte(`{
"object_kind": "merge_request",
"event_type": "merge_request",
"user": {
"id": 2,
"name": "the test",
"username": "test",
"avatar_url": "https://www.gravatar.com/avatar/dd46a756faad4727fb679320751f6dea?s=80&d=identicon",
"email": "test@test.test"
"id": 2251488,
"name": "Anbraten",
"username": "anbraten",
"avatar_url": "https://secure.gravatar.com/avatar/fc9b6fe77c6b732a02925a62a81f05a0?s=80&d=identicon",
"email": "some@mail.info"
},
"project": {
"id": 2,
"name": "Woodpecker",
"id": 32059612,
"name": "woodpecker",
"description": "",
"web_url": "http://10.40.8.5:3200/test/woodpecker",
"avatar_url": null,
"git_ssh_url": "git@10.40.8.5:test/woodpecker.git",
"git_http_url": "http://10.40.8.5:3200/test/woodpecker.git",
"namespace": "the test",
"web_url": "https://gitlab.com/anbraten/woodpecker",
"avatar_url": "http://example.com/uploads/project/avatar/555/Outh-20-Logo.jpg",
"git_ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"git_http_url": "https://gitlab.com/anbraten/woodpecker.git",
"namespace": "Anbraten",
"visibility_level": 20,
"path_with_namespace": "test/woodpecker",
"default_branch": "master",
"ci_config_path": null,
"homepage": "http://10.40.8.5:3200/test/woodpecker",
"url": "git@10.40.8.5:test/woodpecker.git",
"ssh_url": "git@10.40.8.5:test/woodpecker.git",
"http_url": "http://10.40.8.5:3200/test/woodpecker.git"
"path_with_namespace": "anbraten/woodpecker",
"default_branch": "main",
"ci_config_path": "",
"homepage": "https://gitlab.com/anbraten/woodpecker",
"url": "git@gitlab.com:anbraten/woodpecker.git",
"ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"http_url": "https://gitlab.com/anbraten/woodpecker.git"
},
"object_attributes": {
"assignee_id": null,
"author_id": 2,
"created_at": "2021-09-27 05:00:01 UTC",
"assignee_id": 2251488,
"author_id": 2251488,
"created_at": "2022-01-10 15:23:41 UTC",
"description": "",
"head_pipeline_id": 5,
"id": 2,
"iid": 2,
"last_edited_at": null,
"last_edited_by_id": null,
"head_pipeline_id": 449733536,
"id": 134400602,
"iid": 3,
"last_edited_at": "2022-01-17 15:46:23 UTC",
"last_edited_by_id": 2251488,
"merge_commit_sha": null,
"merge_error": null,
"merge_params": {
@ -217,61 +217,61 @@ var ServiceHookMergeRequestBody = []byte(`{
"merge_user_id": null,
"merge_when_pipeline_succeeds": false,
"milestone_id": null,
"source_branch": "masterfdsafds",
"source_project_id": 2,
"source_branch": "anbraten-main-patch-05373",
"source_project_id": 32059612,
"state_id": 1,
"target_branch": "master",
"target_project_id": 2,
"target_branch": "main",
"target_project_id": 32059612,
"time_estimate": 0,
"title": "Update client.go 🎉",
"updated_at": "2021-09-27 05:01:21 UTC",
"updated_by_id": null,
"url": "http://10.40.8.5:3200/test/woodpecker/-/merge_requests/2",
"updated_at": "2022-01-17 15:47:39 UTC",
"updated_by_id": 2251488,
"url": "https://gitlab.com/anbraten/woodpecker/-/merge_requests/3",
"source": {
"id": 2,
"name": "Woodpecker",
"id": 32059612,
"name": "woodpecker",
"description": "",
"web_url": "http://10.40.8.5:3200/test/woodpecker",
"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_http_url": "http://10.40.8.5:3200/test/woodpecker.git",
"namespace": "the test",
"web_url": "https://gitlab.com/anbraten/woodpecker",
"avatar_url": null,
"git_ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"git_http_url": "https://gitlab.com/anbraten/woodpecker.git",
"namespace": "Anbraten",
"visibility_level": 20,
"path_with_namespace": "test/woodpecker",
"default_branch": "develop",
"ci_config_path": null,
"homepage": "http://10.40.8.5:3200/test/woodpecker",
"url": "git@10.40.8.5:test/woodpecker.git",
"ssh_url": "git@10.40.8.5:test/woodpecker.git",
"http_url": "http://10.40.8.5:3200/test/woodpecker.git"
"path_with_namespace": "anbraten/woodpecker",
"default_branch": "main",
"ci_config_path": "",
"homepage": "https://gitlab.com/anbraten/woodpecker",
"url": "git@gitlab.com:anbraten/woodpecker.git",
"ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"http_url": "https://gitlab.com/anbraten/woodpecker.git"
},
"target": {
"id": 2,
"name": "Woodpecker",
"id": 32059612,
"name": "woodpecker",
"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",
"git_ssh_url": "git@10.40.8.5:test/woodpecker.git",
"git_http_url": "http://10.40.8.5:3200/test/woodpecker.git",
"namespace": "the test",
"git_ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"git_http_url": "https://gitlab.com/anbraten/woodpecker.git",
"namespace": "Anbraten",
"visibility_level": 20,
"path_with_namespace": "test/woodpecker",
"default_branch": "develop",
"ci_config_path": null,
"homepage": "http://10.40.8.5:3200/test/woodpecker",
"url": "git@10.40.8.5:test/woodpecker.git",
"ssh_url": "git@10.40.8.5:test/woodpecker.git",
"http_url": "http://10.40.8.5:3200/test/woodpecker.git"
"path_with_namespace": "anbraten/woodpecker",
"default_branch": "main",
"ci_config_path": "",
"homepage": "https://gitlab.com/anbraten/woodpecker",
"url": "git@gitlab.com:anbraten/woodpecker.git",
"ssh_url": "git@gitlab.com:anbraten/woodpecker.git",
"http_url": "https://gitlab.com/anbraten/woodpecker.git"
},
"last_commit": {
"id": "0ab96a10266b95b4b533dcfd98738015fbe70889",
"message": "Update state.go",
"title": "Update state.go",
"timestamp": "2021-09-27T05:01:20+00:00",
"url": "http://10.40.8.5:3200/test/woodpecker/-/commit/0ab96a10266b95b4b533dcfd98738015fbe70889",
"id": "c136499ec574e1034b24c5d306de9acda3005367",
"message": "Update folder/todo.txt",
"title": "Update folder/todo.txt",
"timestamp": "2022-01-17T15:47:38+00:00",
"url": "https://gitlab.com/anbraten/woodpecker/-/commit/c136499ec574e1034b24c5d306de9acda3005367",
"author": {
"name": "the test",
"email": "test@test.test"
"name": "Anbraten",
"email": "some@mail.info"
}
},
"work_in_progress": false,
@ -281,25 +281,36 @@ var ServiceHookMergeRequestBody = []byte(`{
"human_time_change": null,
"human_time_estimate": null,
"assignee_ids": [
2251488
],
"state": "opened",
"blocking_discussions_resolved": true,
"action": "update",
"oldrev": "6ef047571374c96a2bf13c361efd1fb008b0063e"
"oldrev": "8b641937b7340066d882b9d8a8cc5b0573a207de"
},
"labels": [
],
"changes": {
"updated_at": {
"previous": "2021-09-27 05:00:01 UTC",
"current": "2021-09-27 05:01:21 UTC"
"previous": "2022-01-17 15:46:23 UTC",
"current": "2022-01-17 15:47:39 UTC"
}
},
"repository": {
"name": "Woodpecker",
"url": "git@10.40.8.5:test/woodpecker.git",
"name": "woodpecker",
"url": "git@gitlab.com:anbraten/woodpecker.git",
"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
// 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)
}

View file

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

View file

@ -30,6 +30,12 @@ func FromContext(c context.Context) 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
// the Setter interface.
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)
}
}
}