Support changed files for Gitea PRs (#1342)

- add tests to fetch changed files
- ignore error if gitea version is to low
- adjust docs accordingly

Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
qwerty287 2022-10-28 19:17:30 +02:00 committed by GitHub
parent 36e42914fa
commit 8f183c82a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 142 additions and 12 deletions

View file

@ -37,3 +37,8 @@ issues:
linters: linters:
- deadcode - deadcode
- unused - unused
# gin force us to use string as context key
- path: server/store/context.go
linters:
- staticcheck
- revive

View file

@ -412,8 +412,7 @@ when:
:::info :::info
Path conditions are applied only to **push** and **pull_request** events. Path conditions are applied only to **push** and **pull_request** events.
It is currently **only available** for GitHub, GitLab. It is currently **only available** for GitHub, GitLab and Gitea (version 1.18.0 and newer)
Gitea only supports **push** at the moment ([go-gitea/gitea#18228](https://github.com/go-gitea/gitea/pull/18228)).
::: :::
Execute a step only on a pipeline with certain files being changed: Execute a step only on a pipeline with certain files being changed:

View file

@ -12,4 +12,4 @@
| [Multi pipeline](../../20-usage/25-multi-pipeline.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | | [Multi pipeline](../../20-usage/25-multi-pipeline.md) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: |
| [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: | :x: | :x: | :x: | | [when.path filter](../../20-usage/20-pipeline-syntax.md#path) | :white_check_mark: | :white_check_mark:¹ | :white_check_mark: | :x: | :x: | :x: | :x: |
¹) [except for pull requests](https://github.com/woodpecker-ci/woodpecker/issues/754) ¹) for Gitea versions 1.17 or lower not for pull requests

2
go.mod
View file

@ -3,7 +3,7 @@ module github.com/woodpecker-ci/woodpecker
go 1.18 go 1.18
require ( require (
code.gitea.io/sdk/gitea v0.15.1-0.20220831004139-a0127ed0e7fe code.gitea.io/sdk/gitea v0.15.1-0.20221016183512-2d9ee57af1e0
codeberg.org/6543/go-yaml2json v0.3.0 codeberg.org/6543/go-yaml2json v0.3.0
github.com/antonmedv/expr v1.9.0 github.com/antonmedv/expr v1.9.0
github.com/bmatcuk/doublestar/v4 v4.2.0 github.com/bmatcuk/doublestar/v4 v4.2.0

4
go.sum
View file

@ -32,8 +32,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
code.gitea.io/sdk/gitea v0.15.1-0.20220831004139-a0127ed0e7fe h1:PeLyxnUZE85QuJtBZ4P8qCQcgWG5Ked67mlNgr0WkCQ= code.gitea.io/sdk/gitea v0.15.1-0.20221016183512-2d9ee57af1e0 h1:AKpsCoOtVrWWBtANM9319pwCB5ihx1Sdvr1HSbAwr54=
code.gitea.io/sdk/gitea v0.15.1-0.20220831004139-a0127ed0e7fe/go.mod h1:aRmrQC3CAHdJAU1LQt0C9zqzqI8tUB/5oQtNE746aYE= code.gitea.io/sdk/gitea v0.15.1-0.20221016183512-2d9ee57af1e0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg=
codeberg.org/6543/go-yaml2json v0.3.0 h1:BlvjmY0Gous8P+rr8aBdgPYnIfUAqFepF8q7Tp0R5t8= codeberg.org/6543/go-yaml2json v0.3.0 h1:BlvjmY0Gous8P+rr8aBdgPYnIfUAqFepF8q7Tp0R5t8=
codeberg.org/6543/go-yaml2json v0.3.0/go.mod h1:mz61q14LWF4ZABrgMEDMmk3t9dPi6zgR1uBh2VKV2RQ= codeberg.org/6543/go-yaml2json v0.3.0/go.mod h1:mz61q14LWF4ZABrgMEDMmk3t9dPi6zgR1uBh2VKV2RQ=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=

View file

@ -28,11 +28,12 @@ func Handler() http.Handler {
e := gin.New() e := gin.New()
e.GET("/api/v1/repos/:owner/:name", getRepo) e.GET("/api/v1/repos/:owner/:name", getRepo)
e.GET("/api/v1/repositories/:id", getRepoByID) e.GET("/api/v1/repositories/:id", getRepoByID)
e.GET("/api/v1/repos/:owner/:name/raw/:commit/:file", getRepoFile) e.GET("/api/v1/repos/:owner/:name/raw/:file", getRepoFile)
e.POST("/api/v1/repos/:owner/:name/hooks", createRepoHook) e.POST("/api/v1/repos/:owner/:name/hooks", createRepoHook)
e.GET("/api/v1/repos/:owner/:name/hooks", listRepoHooks) e.GET("/api/v1/repos/:owner/:name/hooks", listRepoHooks)
e.DELETE("/api/v1/repos/:owner/:name/hooks/:id", deleteRepoHook) e.DELETE("/api/v1/repos/:owner/:name/hooks/:id", deleteRepoHook)
e.POST("/api/v1/repos/:owner/:name/statuses/:commit", createRepoCommitStatus) e.POST("/api/v1/repos/:owner/:name/statuses/:commit", createRepoCommitStatus)
e.GET("/api/v1/repos/:owner/:name/pulls/:index/files", getPRFiles)
e.GET("/api/v1/user/repos", getUserRepos) e.GET("/api/v1/user/repos", getUserRepos)
e.GET("/api/v1/version", getVersion) e.GET("/api/v1/version", getVersion)
@ -69,10 +70,13 @@ func createRepoCommitStatus(c *gin.Context) {
} }
func getRepoFile(c *gin.Context) { func getRepoFile(c *gin.Context) {
if c.Param("file") == "file_not_found" { file := c.Param("file")
ref := c.Query("ref")
if file == "file_not_found" {
c.String(404, "") c.String(404, "")
} }
if c.Param("commit") == "v1.0.0" || c.Param("commit") == "9ecad50" { if ref == "v1.0.0" || ref == "9ecad50" {
c.String(200, repoFilePayload) c.String(200, repoFilePayload)
} }
c.String(404, "") c.String(404, "")
@ -116,7 +120,16 @@ func getUserRepos(c *gin.Context) {
} }
func getVersion(c *gin.Context) { func getVersion(c *gin.Context) {
c.JSON(200, map[string]interface{}{"version": "1.12"}) c.JSON(200, map[string]interface{}{"version": "1.18.0"})
}
func getPRFiles(c *gin.Context) {
page := c.Query("page")
if page == "1" {
c.String(200, prFilesPayload)
} else {
c.String(200, "[]")
}
} }
const listRepoHookPayloads = ` const listRepoHookPayloads = `
@ -175,3 +188,18 @@ const userRepoPayload = `
} }
] ]
` `
const prFilesPayload = `
[
{
"filename": "README.md",
"status": "changed",
"additions": 2,
"deletions": 0,
"changes": 2,
"html_url": "http://localhost/username/repo/src/commit/e79e4b0e8d9dd6f72b70e776c3317db7c19ca0fd/README.md",
"contents_url": "http://localhost:3000/api/v1/repos/username/repo/contents/README.md?ref=e79e4b0e8d9dd6f72b70e776c3317db7c19ca0fd",
"raw_url": "http://localhost/username/repo/raw/commit/e79e4b0e8d9dd6f72b70e776c3317db7c19ca0fd/README.md"
}
]
`

View file

@ -39,6 +39,7 @@ 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"
) )
const ( const (
@ -475,7 +476,23 @@ func (c *Gitea) BranchHead(ctx context.Context, u *model.User, r *model.Repo, br
// Hook parses the incoming Gitea hook and returns the Repository and Pipeline // Hook parses the incoming Gitea hook and returns the Repository and Pipeline
// details. If the hook is unsupported nil values are returned. // details. If the hook is unsupported nil values are returned.
func (c *Gitea) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Pipeline, error) { func (c *Gitea) Hook(ctx context.Context, r *http.Request) (*model.Repo, *model.Pipeline, error) {
return parseHook(r) repo, pipeline, err := parseHook(r)
if err != nil {
return nil, nil, err
}
if pipeline.Event == model.EventPull && len(pipeline.ChangedFiles) == 0 {
index, err := strconv.ParseInt(strings.Split(pipeline.Ref, "/")[2], 10, 64)
if err != nil {
return nil, nil, err
}
pipeline.ChangedFiles, err = c.getChangedFilesForPR(ctx, repo, index)
if err != nil {
log.Error().Err(err).Msgf("could not get changed files for PR %s#%d", repo.FullName, index)
}
}
return repo, pipeline, nil
} }
// OrgMembership returns if user is member of organization and if user // OrgMembership returns if user is member of organization and if user
@ -540,3 +557,46 @@ func getStatus(status model.StatusValue) gitea.StatusState {
return gitea.StatusFailure return gitea.StatusFailure
} }
} }
func (c *Gitea) getChangedFilesForPR(ctx context.Context, repo *model.Repo, index int64) ([]string, error) {
_store, ok := store.TryFromContext(ctx)
if !ok {
log.Error().Msg("could not get store from context")
return []string{}, nil
}
repo, err := _store.GetRepoNameFallback(repo.RemoteID, repo.FullName)
if err != nil {
return nil, err
}
user, err := _store.GetUser(repo.UserID)
if err != nil {
return nil, err
}
client, err := c.newClientToken(ctx, user.Token)
if err != nil {
return nil, err
}
if client.CheckServerVersionConstraint("1.18.0") != nil {
// version too low
log.Debug().Msg("Gitea version does not support getting changed files for PRs")
return []string{}, nil
}
return common.Paginate(func(page int) ([]string, error) {
giteaFiles, _, err := client.ListPullRequestFiles(repo.Owner, repo.Name, index,
gitea.ListPullRequestFilesOptions{ListOptions: gitea.ListOptions{Page: page}})
if err != nil {
return nil, err
}
var files []string
for _, file := range giteaFiles {
files = append(files, file.Filename)
}
return files, nil
})
}

View file

@ -16,15 +16,21 @@
package gitea package gitea
import ( import (
"bytes"
"context" "context"
"net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/franela/goblin" "github.com/franela/goblin"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/stretchr/testify/mock"
"github.com/woodpecker-ci/woodpecker/shared/utils"
"github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/remote/gitea/fixtures" "github.com/woodpecker-ci/woodpecker/server/remote/gitea/fixtures"
"github.com/woodpecker-ci/woodpecker/server/store"
mocks_store "github.com/woodpecker-ci/woodpecker/server/store/mocks"
) )
func Test_gitea(t *testing.T) { func Test_gitea(t *testing.T) {
@ -36,7 +42,9 @@ func Test_gitea(t *testing.T) {
SkipVerify: true, SkipVerify: true,
}) })
ctx := context.Background() mockStore := mocks_store.NewStore(t)
ctx := store.InjectToContext(context.Background(), mockStore)
g := goblin.Goblin(t) g := goblin.Goblin(t)
g.Describe("Gitea", func() { g.Describe("Gitea", func() {
g.After(func() { g.After(func() {
@ -154,6 +162,21 @@ func Test_gitea(t *testing.T) {
g.It("Should return push details") g.It("Should return push details")
g.It("Should handle a parsing error") g.It("Should handle a parsing error")
}) })
g.It("Given a PR hook", func() {
buf := bytes.NewBufferString(fixtures.HookPullRequest)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPullRequest)
mockStore.On("GetRepoNameFallback", mock.Anything, mock.Anything).Return(fakeRepo, nil)
mockStore.On("GetUser", mock.Anything).Return(fakeUser, nil)
r, b, err := c.Hook(ctx, req)
g.Assert(r).IsNotNil()
g.Assert(b).IsNotNil()
g.Assert(err).IsNil()
g.Assert(b.Event).Equal(model.EventPull)
g.Assert(utils.EqualStringSlice(b.ChangedFiles, []string{"README.md"})).IsTrue()
})
}) })
} }

View file

@ -40,6 +40,17 @@ func Test_parser(t *testing.T) {
g.Assert(b).IsNil() g.Assert(b).IsNil()
g.Assert(err).IsNil() g.Assert(err).IsNil()
}) })
g.It("given a PR hook", func() {
buf := bytes.NewBufferString(fixtures.HookPullRequest)
req, _ := http.NewRequest("POST", "/hook", buf)
req.Header = http.Header{}
req.Header.Set(hookEvent, hookPullRequest)
r, b, err := parseHook(req)
g.Assert(r).IsNotNil()
g.Assert(b).IsNotNil()
g.Assert(err).IsNil()
g.Assert(b.Event).Equal(model.EventPull)
})
g.Describe("given a push hook", func() { g.Describe("given a push hook", func() {
g.It("should extract repository and pipeline details", func() { g.It("should extract repository and pipeline details", func() {
buf := bytes.NewBufferString(fixtures.HookPush) buf := bytes.NewBufferString(fixtures.HookPush)

View file

@ -41,3 +41,7 @@ func TryFromContext(c context.Context) (Store, bool) {
func ToContext(c Setter, store Store) { func ToContext(c Setter, store Store) {
c.Set(key, store) c.Set(key, store)
} }
func InjectToContext(ctx context.Context, store Store) context.Context {
return context.WithValue(ctx, key, store)
}