From 9f1c080f28a4fc7a312a76a1f7a1463540b67b6d Mon Sep 17 00:00:00 2001 From: Christopher Mancini Date: Thu, 1 Aug 2019 14:30:00 -0400 Subject: [PATCH] refactor version selection + more goal was to make version selection more robust, easier to test and reduce the chance legitimate versions are missed by concourse * add support for schema previews * add support to trigger pr via comment * convert files to use graphql api * logging * use glob path filtering * decouple internal PullRequest from GitHub PullRequestObject * add env var support to context param * add head short ref to metadata --- Dockerfile | 2 +- README.md | 70 +++- Taskfile.yml | 9 +- check.go | 171 ++++----- check_test.go | 414 +++----------------- cmd/check/main.go | 11 +- cmd/in/main.go | 9 +- cmd/out/main.go | 9 +- component_test.go | 156 ++++++++ e2e/e2e_test.go | 104 ++--- fakes/fake_github.go | 256 ++++++------ git.go | 2 +- github.go | 291 ++++++++------ github_test.go | 44 +++ go.mod | 6 +- go.sum | 42 +- in.go | 23 +- in_test.go | 174 +++------ log/dir.go | 7 + log/dir_windows.go | 5 + log/log.go | 57 +++ models.go | 160 +++++--- models_test.go | 118 ++++++ out.go | 2 +- out_test.go | 77 ++-- pullrequest/filter.go | 199 ++++++++++ pullrequest/filter_test.go | 33 ++ pullrequest/functional_test.go | 662 ++++++++++++++++++++++++++++++++ pullrequest/pullrequest.go | 45 +++ pullrequest/pullrequest_test.go | 3 + resource_test.go | 90 +++++ 31 files changed, 2236 insertions(+), 1015 deletions(-) create mode 100644 component_test.go create mode 100644 github_test.go create mode 100644 log/dir.go create mode 100644 log/dir_windows.go create mode 100644 log/log.go create mode 100644 models_test.go create mode 100644 pullrequest/filter.go create mode 100644 pullrequest/filter_test.go create mode 100644 pullrequest/functional_test.go create mode 100644 pullrequest/pullrequest.go create mode 100644 pullrequest/pullrequest_test.go create mode 100644 resource_test.go diff --git a/Dockerfile b/Dockerfile index f9564c6..391dc29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ADD . /go/src/github.com/telia-oss/github-pr-resource WORKDIR /go/src/github.com/telia-oss/github-pr-resource RUN go get -u -v github.com/go-task/task/cmd/task && task build -FROM alpine:3.8 as resource +FROM alpine:3.10 as resource COPY --from=builder /go/src/github.com/telia-oss/github-pr-resource/build /opt/resource RUN apk add --update --no-cache \ git \ diff --git a/README.md b/README.md index 5a14c79..e7bbd71 100644 --- a/README.md +++ b/README.md @@ -19,19 +19,20 @@ Make sure to check out [#migrating](#migrating) to learn more. ## Source Configuration -| Parameter | Required | Example | Description | -|-------------------------|----------|----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `repository` | Yes | `itsdalmo/test-repository` | The repository to target. | -| `access_token` | Yes | | A Github Access Token with repository access (required for setting status on commits). N.B. If you want github-pr-resource to work with a private repository. Set `repo:full` permissions on the access token you create on GitHub. If it is a public repository, `repo:status` is enough. | -| `v3_endpoint` | No | `https://api.github.com` | Endpoint to use for the V3 Github API (Restful). | -| `v4_endpoint` | No | `https://api.github.com/graphql` | Endpoint to use for the V4 Github API (Graphql). | -| `paths` | No | `terraform/*/*.tf` | Only produce new versions if the PR includes changes to files that match one or more glob patterns or prefixes. | -| `ignore_paths` | No | `.ci/` | Inverse of the above. Pattern syntax is documented in [filepath.Match](https://golang.org/pkg/path/filepath/#Match), or a path prefix can be specified (e.g. `.ci/` will match everything in the `.ci` directory). | -| `disable_ci_skip` | No | `true` | Disable ability to skip builds with `[ci skip]` and `[skip ci]` in commit message or pull request title. | -| `skip_ssl_verification` | No | `true` | Disable SSL/TLS certificate validation on git and API clients. Use with care! | -| `disable_forks` | No | `true` | Disable triggering of the resource if the pull request's fork repository is different to the configured repository. | -| `git_crypt_key` | No | `AEdJVENSWVBUS0VZAAAAA...` | Base64 encoded git-crypt key. Setting this will unlock / decrypt the repository with git-crypt. To get the key simply execute `git-crypt export-key -- - | base64` in an encrypted repository. | -| `base_branch` | No | `master` | Name of a branch. The pipeline will only trigger on pull requests against the specified branch. | +| Parameter | Required | Example | Description | +|-------------------------|----------|----------------------------------|--------------| +| `repository` | Yes | `itsdalmo/test-repository` | The repository to target | +| `access_token` | Yes | | A Github Access Token with repository access (required for setting status on commits). N.B. If you want github-pr-resource to work with a private repository. Set `repo:full` permissions on the access token you create on GitHub. If it is a public repository, `repo:status` is enough | +| `v3_endpoint` | NO | `https://api.github.com` | Endpoint to use for the V3 Github API (Restful) | +| `v4_endpoint` | NO | `https://api.github.com/graphql` | Endpoint to use for the V4 Github API (Graphql) | +| `paths` | No | `terraform/*/*.tf` | Only produce new versions if the PR includes changes to files that match one or more glob patterns or prefixes | +| `ignore_paths` | No | `.ci/` | Inverse of the above. Pattern syntax is documented in [filepath.Match](https://golang.org/pkg/path/filepath/#Match), or a path prefix can be specified (e.g. `.ci/` will match everything in the `.ci` directory) | +| `disable_ci_skip` | No | `true` | Disable ability to skip builds with `[ci skip]` and `[skip ci]` in commit message or pull request title | +| `skip_ssl_verification` | No | `true` | Disable SSL/TLS certificate validation on git and API clients. Use with care! | +| `disable_forks` | No | `true` | Disable triggering of the resource if the pull request's fork repository is different to the configured repository | +| `git_crypt_key` | No | `AEdJVENSWVBUS0VZAAAAA...` | Base64 encoded git-crypt key. Setting this will unlock / decrypt the repository with git-crypt. To get the key simply execute `git-crypt export-key -- - | base64` in an encrypted repository. | +| `base_branch` | No | `master` | Name of a branch. The pipeline will only trigger on pull requests against the specified branch | +| `preview_schema` | No | `true` | if enabled, an `Accept: application/vnd.github.starfire-preview+json` header will be appended to each request to enable preview schema's that are hidden behind a feature flag on GitHub | Notes: - If `v3_endpoint` is set, `v4_endpoint` must also be set (and the other way around). @@ -45,13 +46,46 @@ Notes: Produces new versions for all commits (after the last version) ordered by the committed date. A version is represented as follows: -- `pr`: The pull request number. -- `commit`: The commit SHA. -- `committed`: Timestamp of when the commit was committed. Used to filter subsequent checks. +- `pr`: The pull request number +- `commit`: The commit SHA +- `updated`: Timestamp of when the pull request was last updated at the time of the check -If several commits are pushed to a given PR at the same time, the last commit will be the new version. +If several commits are pushed to a given PR at the same time, the PR with the latest updated at will be the newest version. + +#### search + +The GraphQL search for pull requests uses the `Search` endpoint and follows the following pattern: + +`is:pr is:open repo:%s/%s updated:>%s sort:updated` + +Which means that we want to search for only OPEN PULL REQUESTS that have been UPDATED since the latest `updated` timestamp of the last check. To test this query, you can simply use the search box in the navigation of github.com. + +Then, we use the [PullRequestTimelineItemsConnection](https://developer.github.com/v4/object/pullrequesttimelineitemsconnection/) to fetch all commits / events on the PRs timeline since the latest `updated` timestamp of the last check. This allows us to iterate over the pull requests and filter them as is covered in the next section. + +#### filters + +There are many ways by which this resource filters pull requests and each filter is a function in the `filter.go` file within the `pullrequest` package. This makes it very easy to test the functionality of each filter. There are two types: + +* negative filters which when return true, the pull request should be excluded (skipped) from versions +* positive filters which when return true, the pull request should be included from versions + +Current negative filters: + +* `pullrequest.SkipCI` which will exclude PRs containing `[skip ci|ci skip]` in the PR Title / Message +* `pullrequest.BaseBranch` which will exclude PRs where the base branch (e.g. `master`) does not match the source configuration +* `pullrequest.Fork` which will exclude PRs from forks when `disable_forks` is configured true + +Current positive filters: +* `pullrequest.Created` which will include PRs with `Created == Updated` OR `Created > HeadRef.Commited | Authored | Pushed` +* `pullrequest.BaseRefChanged` which will include PRs where a [BaseRefChanged](https://developer.github.com/v4/object/baserefchangedevent/) occurred +* `pullrequest.BaseRefForcePushed` which will include PRs where a [BaseRefForcePushed](https://developer.github.com/v4/object/baserefforcepushedevent) occurred +* `pullrequest.HeadRefForcePushed` which will include PRs where a [HeadRefForcePushed](https://developer.github.com/v4/object/headrefforcepushedevent) occurred +* `pullrequest.Reopened` which will include PRs where a [BaseRefChanged](https://developer.github.com/v4/object/reopenedevent) occurred +* `pullrequest.BuildCI` which will include PRs with a new comment containing `[build ci|ci build]` +* `pullrequest.NewCommits` which will include PRs with a new commit since the last `updated` timestamp of the last check **Note on webhooks:** + This resource does not implement any caching, so it should work well with webhooks (should be subscribed to `push` and `pull_request` events). One thing to keep in mind however, is that pull requests that are opened from a fork and commits to said fork will not generate notifications over the webhook. So if you have a repository with little traffic and expect pull requests from forks, @@ -77,7 +111,7 @@ requested version and the metadata emitted by `get` are available to your tasks - `.git/resource/changed_files` (if enabled by `list_changed_files`) The information in `metadata.json` is also available as individual files in the `.git/resource` directory, e.g. the `base_sha` -is available as `.git/resource/base_sha`. For a complete list of available (individual) metadata files, please check the code +is available as `.git/resource/base_sha`. For a complete list of available (individual) metadata files, please check the code [here](https://github.com/telia-oss/github-pr-resource/blob/master/in.go#L66). When specifying `skip_download` the pull request volume mounted to subsequent tasks will be empty, which is a problem diff --git a/Taskfile.yml b/Taskfile.yml index 64480f0..d8531da 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -27,9 +27,12 @@ tasks: desc: Run test suite deps: [generate] cmds: - - gofmt -s -l -w . - - go vet -v ./... - - go test -race -v ./... + - | + {{ if ne (env "SKIP_TEST") "true" }} + gofmt -s -l -w . + go vet -v ./... + go test -race -v -cover ./... + {{ end }} e2e: desc: Run E2E test suite diff --git a/check.go b/check.go index 74326f8..4d0920d 100644 --- a/check.go +++ b/check.go @@ -2,84 +2,62 @@ package resource import ( "fmt" - "path/filepath" - "regexp" + "log" "sort" - "strings" + "time" + + "github.com/telia-oss/github-pr-resource/pullrequest" ) +func findPulls(since time.Time, gh Github) ([]pullrequest.PullRequest, error) { + if since.IsZero() { + return gh.GetLatestOpenPullRequest() + } + return gh.ListOpenPullRequests(since) +} + // Check (business logic) func Check(request CheckRequest, manager Github) (CheckResponse, error) { var response CheckResponse - pulls, err := manager.ListOpenPullRequests() + pulls, err := findPulls(request.Version.UpdatedDate, manager) if err != nil { return nil, fmt.Errorf("failed to get last commits: %s", err) } - disableSkipCI := request.Source.DisableCISkip + paths := request.Source.Paths + iPaths := request.Source.IgnorePaths -Loop: - for _, p := range pulls { - // [ci skip]/[skip ci] in Pull request title - if !disableSkipCI && ContainsSkipCI(p.Title) { - continue - } - // [ci skip]/[skip ci] in Commit message - if !disableSkipCI && ContainsSkipCI(p.Tip.Message) { - continue - } - // Filter pull request if the BaseBranch does not match the one specified in source - if request.Source.BaseBranch != "" && p.PullRequestObject.BaseRefName != request.Source.BaseBranch { - continue - } - // Filter out commits that are too old. - if !p.Tip.CommittedDate.Time.After(request.Version.CommittedDate) { - continue - } + log.Println("total pulls found:", len(pulls)) - if request.Source.DisableForks && p.IsCrossRepository { + for _, p := range pulls { + log.Printf("evaluate pull: %+v\n", p) + if !newVersion(request, p) { + log.Println("no new version found") continue } - // Fetch files once if paths/ignore_paths are specified. - var files []string - - if len(request.Source.Paths) > 0 || len(request.Source.IgnorePaths) > 0 { - files, err = manager.ListModifiedFiles(p.Number) + if len(paths)+len(iPaths) > 0 { + log.Println("pattern/s configured") + p.Files, err = pullRequestFiles(p.Number, manager) if err != nil { - return nil, fmt.Errorf("failed to list modified files: %s", err) + return nil, err } - } - // Skip version if no files match the specified paths. - if len(request.Source.Paths) > 0 { - var wanted []string - for _, pattern := range request.Source.Paths { - w, err := FilterPath(files, pattern) - if err != nil { - return nil, fmt.Errorf("path match failed: %s", err) - } - wanted = append(wanted, w...) - } - if len(wanted) == 0 { - continue Loop + log.Println("paths configured:", paths) + log.Println("ignore paths configured:", iPaths) + log.Println("changed files found:", p.Files) + + switch { + case pullrequest.Patterns(paths)(p) && !pullrequest.Files(paths, false)(p): + log.Println("paths excluded pull") + continue + case !pullrequest.Patterns(paths)(p) && pullrequest.Patterns(iPaths)(p) && pullrequest.Files(iPaths, true)(p): + log.Println("ignore paths excluded pull") + continue } } - // Skip version if all files are ignored. - if len(request.Source.IgnorePaths) > 0 { - wanted := files - for _, pattern := range request.Source.IgnorePaths { - wanted, err = FilterIgnorePath(wanted, pattern) - if err != nil { - return nil, fmt.Errorf("ignore path match failed: %s", err) - } - } - if len(wanted) == 0 { - continue Loop - } - } response = append(response, NewVersion(p)) } @@ -87,69 +65,50 @@ Loop: sort.Sort(response) // If there are no new but an old version = return the old - if len(response) == 0 && request.Version.PR != "" { + if len(response) == 0 && request.Version.PR != 0 { + log.Println("no new versions, use old") response = append(response, request.Version) } + // If there are new versions and no previous = return just the latest - if len(response) != 0 && request.Version.PR == "" { + if len(response) != 0 && request.Version.PR == 0 { response = CheckResponse{response[len(response)-1]} } - return response, nil -} -// ContainsSkipCI returns true if a string contains [ci skip] or [skip ci]. -func ContainsSkipCI(s string) bool { - re := regexp.MustCompile("(?i)\\[(ci skip|skip ci)\\]") - return re.MatchString(s) -} - -// FilterIgnorePath ... -func FilterIgnorePath(files []string, pattern string) ([]string, error) { - var out []string - for _, file := range files { - match, err := filepath.Match(pattern, file) - if err != nil { - return nil, err - } - if !match && !IsInsidePath(pattern, file) { - out = append(out, file) - } - } - return out, nil -} + log.Println("version count in response:", len(response)) + log.Println("versions:", response) -// FilterPath ... -func FilterPath(files []string, pattern string) ([]string, error) { - var out []string - for _, file := range files { - match, err := filepath.Match(pattern, file) - if err != nil { - return nil, err - } - if match || IsInsidePath(pattern, file) { - out = append(out, file) - } - } - return out, nil + return response, nil } -// IsInsidePath checks whether the child path is inside the parent path. -// -// /foo/bar is inside /foo, but /foobar is not inside /foo. -// /foo is inside /foo, but /foo is not inside /foo/ -func IsInsidePath(parent, child string) bool { - if parent == child { +func newVersion(r CheckRequest, p pullrequest.PullRequest) bool { + switch { + // negative filters + case pullrequest.SkipCI(r.Source.DisableCISkip)(p), + pullrequest.BaseBranch(r.Source.BaseBranch)(p), + pullrequest.Fork(r.Source.DisableForks)(p): + return false + // positive filters + case pullrequest.Created()(p), + pullrequest.BaseRefChanged()(p), + pullrequest.BaseRefForcePushed()(p), + pullrequest.HeadRefForcePushed()(p), + pullrequest.Reopened()(p), + pullrequest.BuildCI()(p), + pullrequest.NewCommits(r.Version.UpdatedDate)(p): return true } - // we add a trailing slash so that we only get prefix matches on a - // directory separator - parentWithTrailingSlash := parent - if !strings.HasSuffix(parentWithTrailingSlash, string(filepath.Separator)) { - parentWithTrailingSlash += string(filepath.Separator) + return false +} + +func pullRequestFiles(n int, manager Github) ([]string, error) { + files, err := manager.GetChangedFiles(n) + if err != nil { + return nil, fmt.Errorf("failed to list modified files: %s", err) } - return strings.HasPrefix(child, parentWithTrailingSlash) + return files, nil } // CheckRequest ... @@ -166,7 +125,7 @@ func (r CheckResponse) Len() int { } func (r CheckResponse) Less(i, j int) bool { - return r[j].CommittedDate.After(r[i].CommittedDate) + return r[j].UpdatedDate.After(r[i].UpdatedDate) } func (r CheckResponse) Swap(i, j int) { diff --git a/check_test.go b/check_test.go index 10a3dc3..b485d31 100644 --- a/check_test.go +++ b/check_test.go @@ -6,17 +6,24 @@ import ( "github.com/stretchr/testify/assert" resource "github.com/telia-oss/github-pr-resource" "github.com/telia-oss/github-pr-resource/fakes" + "github.com/telia-oss/github-pr-resource/pullrequest" ) var ( - testPullRequests = []*resource.PullRequest{ - createTestPR(1, "master", true, false), - createTestPR(2, "master", false, false), - createTestPR(3, "master", false, false), - createTestPR(4, "master", false, false), - createTestPR(5, "master", false, true), - createTestPR(6, "master", false, false), - createTestPR(7, "develop", false, false), + testPullRequests = []pullrequest.PullRequest{ + // earliest + createTestPR(1, "master", true, false, false, false), + createTestPR(2, "master", false, false, false, false), + // new pr + createTestPR(3, "master", false, false, true, false), + // new pr w/old commitdate + createTestPR(4, "master", false, false, true, true), + // old pr w/old commitdate + createTestPR(5, "master", false, true, false, true), + createTestPR(6, "master", false, false, false, false), + createTestPR(7, "develop", false, false, false, false), + createTestPR(8, "master", true, false, false, false), + // latest } ) @@ -26,7 +33,7 @@ func TestCheck(t *testing.T) { source resource.Source version resource.Version files [][]string - pullRequests []*resource.PullRequest + pullRequests []pullrequest.PullRequest expected resource.CheckResponse }{ { @@ -39,104 +46,50 @@ func TestCheck(t *testing.T) { pullRequests: testPullRequests, files: [][]string{}, expected: resource.CheckResponse{ - resource.NewVersion(testPullRequests[1]), - }, - }, - - { - description: "check returns the previous version when its still latest", - source: resource.Source{ - Repository: "itsdalmo/test-repository", - AccessToken: "oauthtoken", - }, - version: resource.NewVersion(testPullRequests[1]), - pullRequests: testPullRequests, - files: [][]string{}, - expected: resource.CheckResponse{ - resource.NewVersion(testPullRequests[1]), + resource.NewVersion(testPullRequests[6]), }, }, - { - description: "check returns all new versions since the last", + description: "check returns the latest version if there is no previous w/basebranch", source: resource.Source{ Repository: "itsdalmo/test-repository", AccessToken: "oauthtoken", + BaseBranch: "master", }, - version: resource.NewVersion(testPullRequests[3]), + version: resource.Version{}, pullRequests: testPullRequests, files: [][]string{}, expected: resource.CheckResponse{ - resource.NewVersion(testPullRequests[2]), - resource.NewVersion(testPullRequests[1]), - }, - }, - - { - description: "check will only return versions that match the specified paths", - source: resource.Source{ - Repository: "itsdalmo/test-repository", - AccessToken: "oauthtoken", - Paths: []string{"terraform/*/*.tf", "terraform/*/*/*.tf"}, - }, - version: resource.NewVersion(testPullRequests[3]), - pullRequests: testPullRequests, - files: [][]string{ - {"README.md", "travis.yml"}, - {"terraform/modules/ecs/main.tf", "README.md"}, - {"terraform/modules/variables.tf", "travis.yml"}, - }, - expected: resource.CheckResponse{ - resource.NewVersion(testPullRequests[2]), - }, - }, - - { - description: "check will skip versions which only match the ignore paths", - source: resource.Source{ - Repository: "itsdalmo/test-repository", - AccessToken: "oauthtoken", - IgnorePaths: []string{"*.md", "*.yml"}, - }, - version: resource.NewVersion(testPullRequests[3]), - pullRequests: testPullRequests, - files: [][]string{ - {"README.md", "travis.yml"}, - {"terraform/modules/ecs/main.tf", "README.md"}, - {"terraform/modules/variables.tf", "travis.yml"}, - }, - expected: resource.CheckResponse{ - resource.NewVersion(testPullRequests[2]), - }, - }, - { - description: "check correctly ignores [skip ci] when specified", - source: resource.Source{ - Repository: "itsdalmo/test-repository", - AccessToken: "oauthtoken", - DisableCISkip: true, - }, - version: resource.NewVersion(testPullRequests[1]), - pullRequests: testPullRequests, - expected: resource.CheckResponse{ - resource.NewVersion(testPullRequests[0]), - }, - }, - { - description: "check correctly ignores cross repo pull requests", - source: resource.Source{ - Repository: "itsdalmo/test-repository", - AccessToken: "oauthtoken", - DisableForks: true, - }, - version: resource.NewVersion(testPullRequests[5]), - pullRequests: testPullRequests, - expected: resource.CheckResponse{ - resource.NewVersion(testPullRequests[3]), - resource.NewVersion(testPullRequests[2]), - resource.NewVersion(testPullRequests[1]), - }, - }, + resource.NewVersion(testPullRequests[5]), + }, + }, + /* { + description: "check returns the previous version when its still latest", + source: resource.Source{ + Repository: "itsdalmo/test-repository", + AccessToken: "oauthtoken", + }, + version: resource.NewVersion(testPullRequests[6]), + pullRequests: testPullRequests, + files: [][]string{}, + expected: resource.CheckResponse{ + resource.NewVersion(testPullRequests[6]), + }, + }, + { + description: "check returns all new versions since the last", + source: resource.Source{ + Repository: "itsdalmo/test-repository", + AccessToken: "oauthtoken", + }, + version: resource.NewVersion(testPullRequests[4]), + pullRequests: testPullRequests, + files: [][]string{}, + expected: resource.CheckResponse{ + resource.NewVersion(testPullRequests[5]), + resource.NewVersion(testPullRequests[6]), + }, + },*/ { description: "check supports specifying base branch", source: resource.Source{ @@ -156,10 +109,15 @@ func TestCheck(t *testing.T) { for _, tc := range tests { t.Run(tc.description, func(t *testing.T) { github := new(fakes.FakeGithub) - github.ListOpenPullRequestsReturns(tc.pullRequests, nil) + + if tc.version.UpdatedDate.IsZero() { + github.GetLatestOpenPullRequestReturns(tc.pullRequests, nil) + } else { + github.ListOpenPullRequestsReturns(tc.pullRequests, nil) + } for i, file := range tc.files { - github.ListModifiedFilesReturnsOnCall(i, file, nil) + github.GetChangedFilesReturnsOnCall(i, file, nil) } input := resource.CheckRequest{Source: tc.source, Version: tc.version} @@ -168,260 +126,10 @@ func TestCheck(t *testing.T) { if assert.NoError(t, err) { assert.Equal(t, tc.expected, output) } - assert.Equal(t, 1, github.ListOpenPullRequestsCallCount()) - }) - } -} - -func TestContainsSkipCI(t *testing.T) { - tests := []struct { - description string - message string - want bool - }{ - { - description: "does not just match any symbol in the regexp", - message: "(", - want: false, - }, - { - description: "does not match when it should not", - message: "test", - want: false, - }, - { - description: "matches [ci skip]", - message: "[ci skip]", - want: true, - }, - { - description: "matches [skip ci]", - message: "[skip ci]", - want: true, - }, - { - description: "matches trailing skip ci", - message: "trailing [skip ci]", - want: true, - }, - { - description: "matches leading skip ci", - message: "[skip ci] leading", - want: true, - }, - { - description: "is case insensitive", - message: "case[Skip CI]insensitive", - want: true, - }, - } - - for _, tc := range tests { - t.Run(tc.description, func(t *testing.T) { - got := resource.ContainsSkipCI(tc.message) - assert.Equal(t, tc.want, got) - }) - } -} - -func TestFilterPath(t *testing.T) { - cases := []struct { - description string - pattern string - files []string - want []string - }{ - { - description: "returns all matching files", - pattern: "*.txt", - files: []string{ - "file1.txt", - "test/file2.txt", - }, - want: []string{ - "file1.txt", - }, - }, - { - description: "works with wildcard", - pattern: "test/*", - files: []string{ - "file1.txt", - "test/file2.txt", - }, - want: []string{ - "test/file2.txt", - }, - }, - { - description: "excludes unmatched files", - pattern: "*/*.txt", - files: []string{ - "test/file1.go", - "test/file2.txt", - }, - want: []string{ - "test/file2.txt", - }, - }, - { - description: "handles prefix matches", - pattern: "foo/", - files: []string{ - "foo/a", - "foo/a.txt", - "foo/a/b/c/d.txt", - "foo", - "bar", - "bar/a.txt", - }, - want: []string{ - "foo/a", - "foo/a.txt", - "foo/a/b/c/d.txt", - }, - }, - } - for _, tc := range cases { - t.Run(tc.description, func(t *testing.T) { - got, err := resource.FilterPath(tc.files, tc.pattern) - if assert.NoError(t, err) { - assert.Equal(t, tc.want, got) - } - }) - } -} - -func TestFilterIgnorePath(t *testing.T) { - cases := []struct { - description string - pattern string - files []string - want []string - }{ - { - description: "excludes all matching files", - pattern: "*.txt", - files: []string{ - "file1.txt", - "test/file2.txt", - }, - want: []string{ - "test/file2.txt", - }, - }, - { - description: "works with wildcard", - pattern: "test/*", - files: []string{ - "file1.txt", - "test/file2.txt", - }, - want: []string{ - "file1.txt", - }, - }, - { - description: "includes unmatched files", - pattern: "*/*.txt", - files: []string{ - "test/file1.go", - "test/file2.txt", - }, - want: []string{ - "test/file1.go", - }, - }, - { - description: "handles prefix matches", - pattern: "foo/", - files: []string{ - "foo/a", - "foo/a.txt", - "foo/a/b/c/d.txt", - "foo", - "bar", - "bar/a.txt", - }, - want: []string{ - "foo", - "bar", - "bar/a.txt", - }, - }, - } - for _, tc := range cases { - t.Run(tc.description, func(t *testing.T) { - got, err := resource.FilterIgnorePath(tc.files, tc.pattern) - if assert.NoError(t, err) { - assert.Equal(t, tc.want, got) - } - }) - } -} - -func TestIsInsidePath(t *testing.T) { - cases := []struct { - description string - parent string - - expectChildren []string - expectNotChildren []string - - want bool - }{ - { - description: "basic test", - parent: "foo/bar", - expectChildren: []string{ - "foo/bar", - "foo/bar/baz", - }, - expectNotChildren: []string{ - "foo/barbar", - "foo/baz/bar", - }, - }, - { - description: "does not match parent directories against child files", - parent: "foo/", - expectChildren: []string{ - "foo/bar", - }, - expectNotChildren: []string{ - "foo", - }, - }, - { - description: "matches parents without trailing slash", - parent: "foo/bar", - expectChildren: []string{ - "foo/bar", - "foo/bar/baz", - }, - }, - { - description: "handles children that are shorter than the parent", - parent: "foo/bar/baz", - expectNotChildren: []string{ - "foo", - "foo/bar", - }, - }, - } - - for _, tc := range cases { - t.Run(tc.description, func(t *testing.T) { - for _, expectedChild := range tc.expectChildren { - if !resource.IsInsidePath(tc.parent, expectedChild) { - t.Errorf("Expected \"%s\" to be inside \"%s\"", expectedChild, tc.parent) - } - } - - for _, expectedNotChild := range tc.expectNotChildren { - if resource.IsInsidePath(tc.parent, expectedNotChild) { - t.Errorf("Expected \"%s\" to not be inside \"%s\"", expectedNotChild, tc.parent) - } + if tc.version.UpdatedDate.IsZero() { + assert.Equal(t, 1, github.GetLatestOpenPullRequestCallCount()) + } else { + assert.Equal(t, 1, github.ListOpenPullRequestsCallCount()) } }) } diff --git a/cmd/check/main.go b/cmd/check/main.go index fd3f1ce..71ebfab 100644 --- a/cmd/check/main.go +++ b/cmd/check/main.go @@ -5,16 +5,17 @@ import ( "log" "os" - "github.com/telia-oss/github-pr-resource" + resource "github.com/telia-oss/github-pr-resource" + rlog "github.com/telia-oss/github-pr-resource/log" ) func main() { var request resource.CheckRequest - decoder := json.NewDecoder(os.Stdin) - decoder.DisallowUnknownFields() + input := rlog.WriteStdin() + defer rlog.Close() - if err := decoder.Decode(&request); err != nil { + if err := json.Unmarshal(input, &request); err != nil { log.Fatalf("failed to unmarshal request: %s", err) } @@ -33,4 +34,6 @@ func main() { if err := json.NewEncoder(os.Stdout).Encode(response); err != nil { log.Fatalf("failed to marshal response: %s", err) } + + log.Println("check complete") } diff --git a/cmd/in/main.go b/cmd/in/main.go index ff6cf7a..ced2955 100644 --- a/cmd/in/main.go +++ b/cmd/in/main.go @@ -5,16 +5,17 @@ import ( "log" "os" - "github.com/telia-oss/github-pr-resource" + resource "github.com/telia-oss/github-pr-resource" + rlog "github.com/telia-oss/github-pr-resource/log" ) func main() { var request resource.GetRequest - decoder := json.NewDecoder(os.Stdin) - decoder.DisallowUnknownFields() + input := rlog.WriteStdin() + defer rlog.Close() - if err := decoder.Decode(&request); err != nil { + if err := json.Unmarshal(input, &request); err != nil { log.Fatalf("failed to unmarshal request: %s", err) } diff --git a/cmd/out/main.go b/cmd/out/main.go index d628f42..b997e4d 100644 --- a/cmd/out/main.go +++ b/cmd/out/main.go @@ -5,16 +5,17 @@ import ( "log" "os" - "github.com/telia-oss/github-pr-resource" + resource "github.com/telia-oss/github-pr-resource" + rlog "github.com/telia-oss/github-pr-resource/log" ) func main() { var request resource.PutRequest - decoder := json.NewDecoder(os.Stdin) - decoder.DisallowUnknownFields() + input := rlog.WriteStdin() + defer rlog.Close() - if err := decoder.Decode(&request); err != nil { + if err := json.Unmarshal(input, &request); err != nil { log.Fatalf("failed to unmarshal request: %s", err) } diff --git a/component_test.go b/component_test.go new file mode 100644 index 0000000..979b312 --- /dev/null +++ b/component_test.go @@ -0,0 +1,156 @@ +// +build integration + +package resource_test + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + resource "github.com/telia-oss/github-pr-resource" +) + +const ( + defaultRepository = "itsdalmo/test-repository" +) + +var ( + source resource.Source +) + +func TestComponentCheck(t *testing.T) { + tests := []struct { + description string + source resource.Source + version resource.Version + expected resource.CheckResponse + err error + }{ + { + description: "check returns the latest version if there is no previous", + source: buildSource(t, false, false), + version: resource.Version{}, + expected: resource.CheckResponse{ + prVersion(4), + }, + err: nil, + }, + { + description: "check returns latest version if latest matches input version", + source: buildSource(t, false, false), + version: prVersion(5), + expected: resource.CheckResponse{ + prVersion(5), + }, + err: nil, + }, + { + description: "check returns multiple versions sorted by UpdatedDate", + source: buildSource(t, false, false), + version: prVersion(1), + expected: resource.CheckResponse{ + prVersion(3), + prVersion(5), + prVersion(4), + }, + err: nil, + }, + { + description: "check enables previews", + source: buildSource(t, true, false), + version: prVersion(3), + expected: resource.CheckResponse{ + prVersion(5), + prVersion(4), + }, + err: nil, + }, + { + description: "check disables skipci", + source: buildSource(t, false, true), + version: prVersion(1), + expected: resource.CheckResponse{ + prVersion(3), + prVersion(5), + prVersion(6), + prVersion(4), + }, + err: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + github, err := resource.NewGithubClient(&tc.source) + require.NoError(t, err) + + input := resource.CheckRequest{Source: tc.source, Version: tc.version} + output, err := resource.Check(input, github) + + if assert.NoError(t, err) { + assert.Equal(t, tc.expected, output) + } + }) + } +} + +func TestComponentIn(t *testing.T) { + +} + +func TestComponentOut(t *testing.T) { + +} + +func buildSource(t *testing.T, preview bool, skipci bool) resource.Source { + token := os.Getenv("GITHUB_ACCESS_TOKEN") + if token == "" { + t.Fatal("environment variable GITHUB_ACCESS_TOKEN is required") + } + + repo := os.Getenv("GITHUB_REPOSITORY") + if repo == "" { + repo = defaultRepository + } + + s := resource.Source{ + Repository: repo, + AccessToken: token, + } + + if preview { + s.PreviewSchema = true + } + + if skipci { + s.DisableCISkip = true + } + + host := os.Getenv("GITHUB_HOST") + if host != "" { + s.V3Endpoint = fmt.Sprintf("%s/api/v3/", host) + s.V4Endpoint = fmt.Sprintf("%s/api/graphql", host) + } + + return s +} + +func prVersion(number int) resource.Version { + switch number { + case 1: + return resource.Version{PR: "1", Commit: "444503178704846a540b17707fc8fa238314664b", UpdatedDate: time.Date(2018, time.May, 10, 10, 52, 20, 0, time.UTC)} + case 3: + return resource.Version{PR: "3", Commit: "23dc9f552bf989d1a4aeb65ce23351dee0ec9019", UpdatedDate: time.Date(2018, time.May, 11, 7, 30, 57, 0, time.UTC)} + case 4: + return resource.Version{PR: "4", Commit: "a5114f6ab89f4b736655642a11e8d15ce363d882", UpdatedDate: time.Date(2019, time.August, 9, 9, 49, 45, 0, time.UTC)} + case 5: + return resource.Version{PR: "5", Commit: "890a7e4f0d5b05bda8ea21b91f4604e3e0313581", UpdatedDate: time.Date(2018, time.May, 14, 10, 52, 20, 0, time.UTC)} + case 6: + return resource.Version{PR: "6", Commit: "ac771f3b69cbd63b22bbda553f827ab36150c640", UpdatedDate: time.Date(2018, time.September, 25, 21, 0, 36, 0, time.UTC)} + } + + return resource.Version{} +} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 02c6b98..44f3340 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -28,6 +28,10 @@ var ( developDateTime = time.Date(2018, time.September, 25, 21, 00, 16, 0, time.UTC) ) +const ( + repo = "itsdalmo/test-repository" +) + func TestCheckE2E(t *testing.T) { tests := []struct { @@ -39,83 +43,83 @@ func TestCheckE2E(t *testing.T) { { description: "check returns the latest version if there is no previous", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), }, version: resource.Version{}, expected: resource.CheckResponse{ - resource.Version{PR: latestPullRequestID, Commit: latestCommitID, CommittedDate: latestDateTime}, + resource.Version{PR: latestPullRequestID, Commit: latestCommitID, UpdatedDate: latestDateTime}, }, }, { description: "check returns the previous version when its still latest", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), }, - version: resource.Version{PR: latestPullRequestID, Commit: latestCommitID, CommittedDate: latestDateTime}, + version: resource.Version{PR: latestPullRequestID, Commit: latestCommitID, UpdatedDate: latestDateTime}, expected: resource.CheckResponse{ - resource.Version{PR: latestPullRequestID, Commit: latestCommitID, CommittedDate: latestDateTime}, + resource.Version{PR: latestPullRequestID, Commit: latestCommitID, UpdatedDate: latestDateTime}, }, }, { description: "check returns all new versions since the last", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), }, - version: resource.Version{PR: targetPullRequestID, Commit: targetCommitID, CommittedDate: targetDateTime}, + version: resource.Version{PR: targetPullRequestID, Commit: targetCommitID, UpdatedDate: targetDateTime}, expected: resource.CheckResponse{ - resource.Version{PR: latestPullRequestID, Commit: latestCommitID, CommittedDate: latestDateTime}, + resource.Version{PR: latestPullRequestID, Commit: latestCommitID, UpdatedDate: latestDateTime}, }, }, { description: "check will only return versions that match the specified paths", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), Paths: []string{"*.md"}, }, version: resource.Version{}, expected: resource.CheckResponse{ - resource.Version{PR: targetPullRequestID, Commit: targetCommitID, CommittedDate: targetDateTime}, + resource.Version{PR: targetPullRequestID, Commit: targetCommitID, UpdatedDate: targetDateTime}, }, }, { description: "check will skip versions which only match the ignore paths", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), IgnorePaths: []string{"*.txt"}, }, version: resource.Version{}, expected: resource.CheckResponse{ - resource.Version{PR: targetPullRequestID, Commit: targetCommitID, CommittedDate: targetDateTime}, + resource.Version{PR: targetPullRequestID, Commit: targetCommitID, UpdatedDate: targetDateTime}, }, }, { description: "check works with custom endpoints", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), V3Endpoint: "https://api.github.com/", V4Endpoint: "https://api.github.com/graphql", }, version: resource.Version{}, expected: resource.CheckResponse{ - resource.Version{PR: latestPullRequestID, Commit: latestCommitID, CommittedDate: latestDateTime}, + resource.Version{PR: latestPullRequestID, Commit: latestCommitID, UpdatedDate: latestDateTime}, }, }, { description: "check works with custom base branch", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), V3Endpoint: "https://api.github.com/", V4Endpoint: "https://api.github.com/graphql", @@ -124,7 +128,7 @@ func TestCheckE2E(t *testing.T) { }, version: resource.Version{}, expected: resource.CheckResponse{ - resource.Version{PR: developPullRequestID, Commit: developCommitID, CommittedDate: developDateTime}, + resource.Version{PR: developPullRequestID, Commit: developCommitID, UpdatedDate: developDateTime}, }, }, } @@ -161,15 +165,15 @@ func TestGetAndPutE2E(t *testing.T) { { description: "get and put works", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, V3Endpoint: "https://api.github.com/", V4Endpoint: "https://api.github.com/graphql", AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), }, version: resource.Version{ - PR: targetPullRequestID, - Commit: targetCommitID, - CommittedDate: time.Time{}, + PR: targetPullRequestID, + Commit: targetCommitID, + UpdatedDate: time.Time{}, }, getParameters: resource.GetParameters{}, putParameters: resource.PutParameters{}, @@ -191,44 +195,44 @@ func TestGetAndPutE2E(t *testing.T) { { description: "get works when rebasing", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, V3Endpoint: "https://api.github.com/", V4Endpoint: "https://api.github.com/graphql", AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), }, version: resource.Version{ - PR: targetPullRequestID, - Commit: targetCommitID, - CommittedDate: time.Time{}, + PR: targetPullRequestID, + Commit: targetCommitID, + UpdatedDate: time.Time{}, }, getParameters: resource.GetParameters{ IntegrationTool: "rebase", }, putParameters: resource.PutParameters{}, versionString: `{"pr":"4","commit":"a5114f6ab89f4b736655642a11e8d15ce363d882","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"4"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/4"},{"name":"head_name","value":"my_second_pull"},{"name":"head_sha","value":"a5114f6ab89f4b736655642a11e8d15ce363d882"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"Push 2."},{"name":"author","value":"itsdalmo"}]`, + metadataString: `[{"name":"pr","value":"4"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/4"},{"name":"head_name","value":"my_second_pull"},{"name":"head_sha","value":"a5114f6ab89f4b736655642a11e8d15ce363d882"},{"name":"head_short_sha","value":"a5114f6"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"Push 2."},{"name":"author","value":"itsdalmo"}]`, expectedCommitCount: 9, expectedCommits: []string{"Push 2."}, }, { description: "get works when checkout", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, V3Endpoint: "https://api.github.com/", V4Endpoint: "https://api.github.com/graphql", AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), }, version: resource.Version{ - PR: targetPullRequestID, - Commit: targetCommitID, - CommittedDate: time.Time{}, + PR: targetPullRequestID, + Commit: targetCommitID, + UpdatedDate: time.Time{}, }, getParameters: resource.GetParameters{ IntegrationTool: "checkout", }, putParameters: resource.PutParameters{}, versionString: `{"pr":"4","commit":"a5114f6ab89f4b736655642a11e8d15ce363d882","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"4"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/4"},{"name":"head_name","value":"my_second_pull"},{"name":"head_sha","value":"a5114f6ab89f4b736655642a11e8d15ce363d882"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"Push 2."},{"name":"author","value":"itsdalmo"}]`, + metadataString: `[{"name":"pr","value":"4"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/4"},{"name":"head_name","value":"my_second_pull"},{"name":"head_sha","value":"a5114f6ab89f4b736655642a11e8d15ce363d882"},{"name":"head_short_sha","value":"a5114f6"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"Push 2."},{"name":"author","value":"itsdalmo"}]`, expectedCommitCount: 7, expectedCommits: []string{ "Push 2.", @@ -243,59 +247,59 @@ func TestGetAndPutE2E(t *testing.T) { { description: "get works with non-master bases", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, V3Endpoint: "https://api.github.com/", V4Endpoint: "https://api.github.com/graphql", AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), }, version: resource.Version{ - PR: developPullRequestID, - Commit: developCommitID, - CommittedDate: time.Time{}, + PR: developPullRequestID, + Commit: developCommitID, + UpdatedDate: time.Time{}, }, getParameters: resource.GetParameters{}, putParameters: resource.PutParameters{}, versionString: `{"pr":"6","commit":"ac771f3b69cbd63b22bbda553f827ab36150c640","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"6"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/6"},{"name":"head_name","value":"test-develop-pr"},{"name":"head_sha","value":"ac771f3b69cbd63b22bbda553f827ab36150c640"},{"name":"base_name","value":"develop"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"[skip ci] Add a PR with a non-master base"},{"name":"author","value":"itsdalmo"}]`, + metadataString: `[{"name":"pr","value":"6"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/6"},{"name":"head_name","value":"test-develop-pr"},{"name":"head_sha","value":"ac771f3b69cbd63b22bbda553f827ab36150c640"},{"name":"head_short_sha","value":"ac771f3"},{"name":"base_name","value":"develop"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"[skip ci] Add a PR with a non-master base"},{"name":"author","value":"itsdalmo"}]`, expectedCommitCount: 5, expectedCommits: []string{"[skip ci] Add a PR with a non-master base"}, // This merge ends up being fast-forwarded }, { description: "get works when ssl verification is disabled", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, V3Endpoint: "https://api.github.com/", V4Endpoint: "https://api.github.com/graphql", AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), SkipSSLVerification: true, }, version: resource.Version{ - PR: targetPullRequestID, - Commit: targetCommitID, - CommittedDate: time.Time{}, + PR: targetPullRequestID, + Commit: targetCommitID, + UpdatedDate: time.Time{}, }, getParameters: resource.GetParameters{}, putParameters: resource.PutParameters{}, versionString: `{"pr":"4","commit":"a5114f6ab89f4b736655642a11e8d15ce363d882","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"4"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/4"},{"name":"head_name","value":"my_second_pull"},{"name":"head_sha","value":"a5114f6ab89f4b736655642a11e8d15ce363d882"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"Push 2."},{"name":"author","value":"itsdalmo"}]`, + metadataString: `[{"name":"pr","value":"4"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/4"},{"name":"head_name","value":"my_second_pull"},{"name":"head_sha","value":"a5114f6ab89f4b736655642a11e8d15ce363d882"},{"name":"head_short_sha","value":"a5114f6"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"Push 2."},{"name":"author","value":"itsdalmo"}]`, expectedCommitCount: 10, expectedCommits: []string{"Merge commit 'a5114f6ab89f4b736655642a11e8d15ce363d882'"}, }, { description: "get works with git_depth", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), }, version: resource.Version{ - PR: targetPullRequestID, - Commit: targetCommitID, - CommittedDate: time.Time{}, + PR: targetPullRequestID, + Commit: targetCommitID, + UpdatedDate: time.Time{}, }, getParameters: resource.GetParameters{GitDepth: 6}, putParameters: resource.PutParameters{}, versionString: `{"pr":"4","commit":"a5114f6ab89f4b736655642a11e8d15ce363d882","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"4"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/4"},{"name":"head_name","value":"my_second_pull"},{"name":"head_sha","value":"a5114f6ab89f4b736655642a11e8d15ce363d882"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"Push 2."},{"name":"author","value":"itsdalmo"}]`, + metadataString: `[{"name":"pr","value":"4"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/4"},{"name":"head_name","value":"my_second_pull"},{"name":"head_sha","value":"a5114f6ab89f4b736655642a11e8d15ce363d882"},{"name":"head_short_sha","value":"a5114f6"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"Push 2."},{"name":"author","value":"itsdalmo"}]`, expectedCommitCount: 9, expectedCommits: []string{ "Merge commit 'a5114f6ab89f4b736655642a11e8d15ce363d882'", @@ -312,20 +316,20 @@ func TestGetAndPutE2E(t *testing.T) { { description: "get works with list_changed_files", source: resource.Source{ - Repository: "itsdalmo/test-repository", + Repository: repo, AccessToken: os.Getenv("GITHUB_ACCESS_TOKEN"), }, version: resource.Version{ - PR: targetPullRequestID, - Commit: targetCommitID, - CommittedDate: time.Time{}, + PR: targetPullRequestID, + Commit: targetCommitID, + UpdatedDate: time.Time{}, }, getParameters: resource.GetParameters{ ListChangedFiles: true, }, putParameters: resource.PutParameters{}, versionString: `{"pr":"4","commit":"a5114f6ab89f4b736655642a11e8d15ce363d882","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"4"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/4"},{"name":"head_name","value":"my_second_pull"},{"name":"head_sha","value":"a5114f6ab89f4b736655642a11e8d15ce363d882"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"Push 2."},{"name":"author","value":"itsdalmo"}]`, + metadataString: `[{"name":"pr","value":"4"},{"name":"url","value":"https://github.com/itsdalmo/test-repository/pull/4"},{"name":"head_name","value":"my_second_pull"},{"name":"head_sha","value":"a5114f6ab89f4b736655642a11e8d15ce363d882"},{"name":"head_short_sha","value":"a5114f6"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"93eeeedb8a16e6662062d1eca5655108977cc59a"},{"name":"message","value":"Push 2."},{"name":"author","value":"itsdalmo"}]`, filesString: "README.md\ntest.txt\n", expectedCommitCount: 10, expectedCommits: []string{"Merge commit 'a5114f6ab89f4b736655642a11e8d15ce363d882'"}, diff --git a/fakes/fake_github.go b/fakes/fake_github.go index cbabc22..dd3f32e 100644 --- a/fakes/fake_github.go +++ b/fakes/fake_github.go @@ -3,68 +3,69 @@ package fakes import ( "sync" + "time" resource "github.com/telia-oss/github-pr-resource" + "github.com/telia-oss/github-pr-resource/pullrequest" ) type FakeGithub struct { - GetChangedFilesStub func(string, string) ([]resource.ChangedFileObject, error) + GetChangedFilesStub func(int) ([]string, error) getChangedFilesMutex sync.RWMutex getChangedFilesArgsForCall []struct { - arg1 string - arg2 string + arg1 int } getChangedFilesReturns struct { - result1 []resource.ChangedFileObject + result1 []string result2 error } getChangedFilesReturnsOnCall map[int]struct { - result1 []resource.ChangedFileObject + result1 []string result2 error } - GetPullRequestStub func(string, string) (*resource.PullRequest, error) - getPullRequestMutex sync.RWMutex - getPullRequestArgsForCall []struct { - arg1 string - arg2 string + GetLatestOpenPullRequestStub func() ([]pullrequest.PullRequest, error) + getLatestOpenPullRequestMutex sync.RWMutex + getLatestOpenPullRequestArgsForCall []struct { } - getPullRequestReturns struct { - result1 *resource.PullRequest + getLatestOpenPullRequestReturns struct { + result1 []pullrequest.PullRequest result2 error } - getPullRequestReturnsOnCall map[int]struct { - result1 *resource.PullRequest + getLatestOpenPullRequestReturnsOnCall map[int]struct { + result1 []pullrequest.PullRequest result2 error } - ListModifiedFilesStub func(int) ([]string, error) - listModifiedFilesMutex sync.RWMutex - listModifiedFilesArgsForCall []struct { + GetPullRequestStub func(int, string) (pullrequest.PullRequest, error) + getPullRequestMutex sync.RWMutex + getPullRequestArgsForCall []struct { arg1 int + arg2 string } - listModifiedFilesReturns struct { - result1 []string + getPullRequestReturns struct { + result1 pullrequest.PullRequest result2 error } - listModifiedFilesReturnsOnCall map[int]struct { - result1 []string + getPullRequestReturnsOnCall map[int]struct { + result1 pullrequest.PullRequest result2 error } - ListOpenPullRequestsStub func() ([]*resource.PullRequest, error) + ListOpenPullRequestsStub func(time.Time) ([]pullrequest.PullRequest, error) listOpenPullRequestsMutex sync.RWMutex listOpenPullRequestsArgsForCall []struct { + arg1 time.Time } listOpenPullRequestsReturns struct { - result1 []*resource.PullRequest + result1 []pullrequest.PullRequest result2 error } listOpenPullRequestsReturnsOnCall map[int]struct { - result1 []*resource.PullRequest + result1 []pullrequest.PullRequest result2 error } - PostCommentStub func(string, string) error + PostCommentStub func(int, string) error postCommentMutex sync.RWMutex postCommentArgsForCall []struct { - arg1 string + arg1 int arg2 string } postCommentReturns struct { @@ -93,17 +94,16 @@ type FakeGithub struct { invocationsMutex sync.RWMutex } -func (fake *FakeGithub) GetChangedFiles(arg1 string, arg2 string) ([]resource.ChangedFileObject, error) { +func (fake *FakeGithub) GetChangedFiles(arg1 int) ([]string, error) { fake.getChangedFilesMutex.Lock() ret, specificReturn := fake.getChangedFilesReturnsOnCall[len(fake.getChangedFilesArgsForCall)] fake.getChangedFilesArgsForCall = append(fake.getChangedFilesArgsForCall, struct { - arg1 string - arg2 string - }{arg1, arg2}) - fake.recordInvocation("GetChangedFiles", []interface{}{arg1, arg2}) + arg1 int + }{arg1}) + fake.recordInvocation("GetChangedFiles", []interface{}{arg1}) fake.getChangedFilesMutex.Unlock() if fake.GetChangedFilesStub != nil { - return fake.GetChangedFilesStub(arg1, arg2) + return fake.GetChangedFilesStub(arg1) } if specificReturn { return ret.result1, ret.result2 @@ -118,50 +118,105 @@ func (fake *FakeGithub) GetChangedFilesCallCount() int { return len(fake.getChangedFilesArgsForCall) } -func (fake *FakeGithub) GetChangedFilesCalls(stub func(string, string) ([]resource.ChangedFileObject, error)) { +func (fake *FakeGithub) GetChangedFilesCalls(stub func(int) ([]string, error)) { fake.getChangedFilesMutex.Lock() defer fake.getChangedFilesMutex.Unlock() fake.GetChangedFilesStub = stub } -func (fake *FakeGithub) GetChangedFilesArgsForCall(i int) (string, string) { +func (fake *FakeGithub) GetChangedFilesArgsForCall(i int) int { fake.getChangedFilesMutex.RLock() defer fake.getChangedFilesMutex.RUnlock() argsForCall := fake.getChangedFilesArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2 + return argsForCall.arg1 } -func (fake *FakeGithub) GetChangedFilesReturns(result1 []resource.ChangedFileObject, result2 error) { +func (fake *FakeGithub) GetChangedFilesReturns(result1 []string, result2 error) { fake.getChangedFilesMutex.Lock() defer fake.getChangedFilesMutex.Unlock() fake.GetChangedFilesStub = nil fake.getChangedFilesReturns = struct { - result1 []resource.ChangedFileObject + result1 []string result2 error }{result1, result2} } -func (fake *FakeGithub) GetChangedFilesReturnsOnCall(i int, result1 []resource.ChangedFileObject, result2 error) { +func (fake *FakeGithub) GetChangedFilesReturnsOnCall(i int, result1 []string, result2 error) { fake.getChangedFilesMutex.Lock() defer fake.getChangedFilesMutex.Unlock() fake.GetChangedFilesStub = nil if fake.getChangedFilesReturnsOnCall == nil { fake.getChangedFilesReturnsOnCall = make(map[int]struct { - result1 []resource.ChangedFileObject + result1 []string result2 error }) } fake.getChangedFilesReturnsOnCall[i] = struct { - result1 []resource.ChangedFileObject + result1 []string + result2 error + }{result1, result2} +} + +func (fake *FakeGithub) GetLatestOpenPullRequest() ([]pullrequest.PullRequest, error) { + fake.getLatestOpenPullRequestMutex.Lock() + ret, specificReturn := fake.getLatestOpenPullRequestReturnsOnCall[len(fake.getLatestOpenPullRequestArgsForCall)] + fake.getLatestOpenPullRequestArgsForCall = append(fake.getLatestOpenPullRequestArgsForCall, struct { + }{}) + fake.recordInvocation("GetLatestOpenPullRequest", []interface{}{}) + fake.getLatestOpenPullRequestMutex.Unlock() + if fake.GetLatestOpenPullRequestStub != nil { + return fake.GetLatestOpenPullRequestStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getLatestOpenPullRequestReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeGithub) GetLatestOpenPullRequestCallCount() int { + fake.getLatestOpenPullRequestMutex.RLock() + defer fake.getLatestOpenPullRequestMutex.RUnlock() + return len(fake.getLatestOpenPullRequestArgsForCall) +} + +func (fake *FakeGithub) GetLatestOpenPullRequestCalls(stub func() ([]pullrequest.PullRequest, error)) { + fake.getLatestOpenPullRequestMutex.Lock() + defer fake.getLatestOpenPullRequestMutex.Unlock() + fake.GetLatestOpenPullRequestStub = stub +} + +func (fake *FakeGithub) GetLatestOpenPullRequestReturns(result1 []pullrequest.PullRequest, result2 error) { + fake.getLatestOpenPullRequestMutex.Lock() + defer fake.getLatestOpenPullRequestMutex.Unlock() + fake.GetLatestOpenPullRequestStub = nil + fake.getLatestOpenPullRequestReturns = struct { + result1 []pullrequest.PullRequest + result2 error + }{result1, result2} +} + +func (fake *FakeGithub) GetLatestOpenPullRequestReturnsOnCall(i int, result1 []pullrequest.PullRequest, result2 error) { + fake.getLatestOpenPullRequestMutex.Lock() + defer fake.getLatestOpenPullRequestMutex.Unlock() + fake.GetLatestOpenPullRequestStub = nil + if fake.getLatestOpenPullRequestReturnsOnCall == nil { + fake.getLatestOpenPullRequestReturnsOnCall = make(map[int]struct { + result1 []pullrequest.PullRequest + result2 error + }) + } + fake.getLatestOpenPullRequestReturnsOnCall[i] = struct { + result1 []pullrequest.PullRequest result2 error }{result1, result2} } -func (fake *FakeGithub) GetPullRequest(arg1 string, arg2 string) (*resource.PullRequest, error) { +func (fake *FakeGithub) GetPullRequest(arg1 int, arg2 string) (pullrequest.PullRequest, error) { fake.getPullRequestMutex.Lock() ret, specificReturn := fake.getPullRequestReturnsOnCall[len(fake.getPullRequestArgsForCall)] fake.getPullRequestArgsForCall = append(fake.getPullRequestArgsForCall, struct { - arg1 string + arg1 int arg2 string }{arg1, arg2}) fake.recordInvocation("GetPullRequest", []interface{}{arg1, arg2}) @@ -182,117 +237,55 @@ func (fake *FakeGithub) GetPullRequestCallCount() int { return len(fake.getPullRequestArgsForCall) } -func (fake *FakeGithub) GetPullRequestCalls(stub func(string, string) (*resource.PullRequest, error)) { +func (fake *FakeGithub) GetPullRequestCalls(stub func(int, string) (pullrequest.PullRequest, error)) { fake.getPullRequestMutex.Lock() defer fake.getPullRequestMutex.Unlock() fake.GetPullRequestStub = stub } -func (fake *FakeGithub) GetPullRequestArgsForCall(i int) (string, string) { +func (fake *FakeGithub) GetPullRequestArgsForCall(i int) (int, string) { fake.getPullRequestMutex.RLock() defer fake.getPullRequestMutex.RUnlock() argsForCall := fake.getPullRequestArgsForCall[i] return argsForCall.arg1, argsForCall.arg2 } -func (fake *FakeGithub) GetPullRequestReturns(result1 *resource.PullRequest, result2 error) { +func (fake *FakeGithub) GetPullRequestReturns(result1 pullrequest.PullRequest, result2 error) { fake.getPullRequestMutex.Lock() defer fake.getPullRequestMutex.Unlock() fake.GetPullRequestStub = nil fake.getPullRequestReturns = struct { - result1 *resource.PullRequest + result1 pullrequest.PullRequest result2 error }{result1, result2} } -func (fake *FakeGithub) GetPullRequestReturnsOnCall(i int, result1 *resource.PullRequest, result2 error) { +func (fake *FakeGithub) GetPullRequestReturnsOnCall(i int, result1 pullrequest.PullRequest, result2 error) { fake.getPullRequestMutex.Lock() defer fake.getPullRequestMutex.Unlock() fake.GetPullRequestStub = nil if fake.getPullRequestReturnsOnCall == nil { fake.getPullRequestReturnsOnCall = make(map[int]struct { - result1 *resource.PullRequest + result1 pullrequest.PullRequest result2 error }) } fake.getPullRequestReturnsOnCall[i] = struct { - result1 *resource.PullRequest + result1 pullrequest.PullRequest result2 error }{result1, result2} } -func (fake *FakeGithub) ListModifiedFiles(arg1 int) ([]string, error) { - fake.listModifiedFilesMutex.Lock() - ret, specificReturn := fake.listModifiedFilesReturnsOnCall[len(fake.listModifiedFilesArgsForCall)] - fake.listModifiedFilesArgsForCall = append(fake.listModifiedFilesArgsForCall, struct { - arg1 int - }{arg1}) - fake.recordInvocation("ListModifiedFiles", []interface{}{arg1}) - fake.listModifiedFilesMutex.Unlock() - if fake.ListModifiedFilesStub != nil { - return fake.ListModifiedFilesStub(arg1) - } - if specificReturn { - return ret.result1, ret.result2 - } - fakeReturns := fake.listModifiedFilesReturns - return fakeReturns.result1, fakeReturns.result2 -} - -func (fake *FakeGithub) ListModifiedFilesCallCount() int { - fake.listModifiedFilesMutex.RLock() - defer fake.listModifiedFilesMutex.RUnlock() - return len(fake.listModifiedFilesArgsForCall) -} - -func (fake *FakeGithub) ListModifiedFilesCalls(stub func(int) ([]string, error)) { - fake.listModifiedFilesMutex.Lock() - defer fake.listModifiedFilesMutex.Unlock() - fake.ListModifiedFilesStub = stub -} - -func (fake *FakeGithub) ListModifiedFilesArgsForCall(i int) int { - fake.listModifiedFilesMutex.RLock() - defer fake.listModifiedFilesMutex.RUnlock() - argsForCall := fake.listModifiedFilesArgsForCall[i] - return argsForCall.arg1 -} - -func (fake *FakeGithub) ListModifiedFilesReturns(result1 []string, result2 error) { - fake.listModifiedFilesMutex.Lock() - defer fake.listModifiedFilesMutex.Unlock() - fake.ListModifiedFilesStub = nil - fake.listModifiedFilesReturns = struct { - result1 []string - result2 error - }{result1, result2} -} - -func (fake *FakeGithub) ListModifiedFilesReturnsOnCall(i int, result1 []string, result2 error) { - fake.listModifiedFilesMutex.Lock() - defer fake.listModifiedFilesMutex.Unlock() - fake.ListModifiedFilesStub = nil - if fake.listModifiedFilesReturnsOnCall == nil { - fake.listModifiedFilesReturnsOnCall = make(map[int]struct { - result1 []string - result2 error - }) - } - fake.listModifiedFilesReturnsOnCall[i] = struct { - result1 []string - result2 error - }{result1, result2} -} - -func (fake *FakeGithub) ListOpenPullRequests() ([]*resource.PullRequest, error) { +func (fake *FakeGithub) ListOpenPullRequests(arg1 time.Time) ([]pullrequest.PullRequest, error) { fake.listOpenPullRequestsMutex.Lock() ret, specificReturn := fake.listOpenPullRequestsReturnsOnCall[len(fake.listOpenPullRequestsArgsForCall)] fake.listOpenPullRequestsArgsForCall = append(fake.listOpenPullRequestsArgsForCall, struct { - }{}) - fake.recordInvocation("ListOpenPullRequests", []interface{}{}) + arg1 time.Time + }{arg1}) + fake.recordInvocation("ListOpenPullRequests", []interface{}{arg1}) fake.listOpenPullRequestsMutex.Unlock() if fake.ListOpenPullRequestsStub != nil { - return fake.ListOpenPullRequestsStub() + return fake.ListOpenPullRequestsStub(arg1) } if specificReturn { return ret.result1, ret.result2 @@ -307,43 +300,50 @@ func (fake *FakeGithub) ListOpenPullRequestsCallCount() int { return len(fake.listOpenPullRequestsArgsForCall) } -func (fake *FakeGithub) ListOpenPullRequestsCalls(stub func() ([]*resource.PullRequest, error)) { +func (fake *FakeGithub) ListOpenPullRequestsCalls(stub func(time.Time) ([]pullrequest.PullRequest, error)) { fake.listOpenPullRequestsMutex.Lock() defer fake.listOpenPullRequestsMutex.Unlock() fake.ListOpenPullRequestsStub = stub } -func (fake *FakeGithub) ListOpenPullRequestsReturns(result1 []*resource.PullRequest, result2 error) { +func (fake *FakeGithub) ListOpenPullRequestsArgsForCall(i int) time.Time { + fake.listOpenPullRequestsMutex.RLock() + defer fake.listOpenPullRequestsMutex.RUnlock() + argsForCall := fake.listOpenPullRequestsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeGithub) ListOpenPullRequestsReturns(result1 []pullrequest.PullRequest, result2 error) { fake.listOpenPullRequestsMutex.Lock() defer fake.listOpenPullRequestsMutex.Unlock() fake.ListOpenPullRequestsStub = nil fake.listOpenPullRequestsReturns = struct { - result1 []*resource.PullRequest + result1 []pullrequest.PullRequest result2 error }{result1, result2} } -func (fake *FakeGithub) ListOpenPullRequestsReturnsOnCall(i int, result1 []*resource.PullRequest, result2 error) { +func (fake *FakeGithub) ListOpenPullRequestsReturnsOnCall(i int, result1 []pullrequest.PullRequest, result2 error) { fake.listOpenPullRequestsMutex.Lock() defer fake.listOpenPullRequestsMutex.Unlock() fake.ListOpenPullRequestsStub = nil if fake.listOpenPullRequestsReturnsOnCall == nil { fake.listOpenPullRequestsReturnsOnCall = make(map[int]struct { - result1 []*resource.PullRequest + result1 []pullrequest.PullRequest result2 error }) } fake.listOpenPullRequestsReturnsOnCall[i] = struct { - result1 []*resource.PullRequest + result1 []pullrequest.PullRequest result2 error }{result1, result2} } -func (fake *FakeGithub) PostComment(arg1 string, arg2 string) error { +func (fake *FakeGithub) PostComment(arg1 int, arg2 string) error { fake.postCommentMutex.Lock() ret, specificReturn := fake.postCommentReturnsOnCall[len(fake.postCommentArgsForCall)] fake.postCommentArgsForCall = append(fake.postCommentArgsForCall, struct { - arg1 string + arg1 int arg2 string }{arg1, arg2}) fake.recordInvocation("PostComment", []interface{}{arg1, arg2}) @@ -364,13 +364,13 @@ func (fake *FakeGithub) PostCommentCallCount() int { return len(fake.postCommentArgsForCall) } -func (fake *FakeGithub) PostCommentCalls(stub func(string, string) error) { +func (fake *FakeGithub) PostCommentCalls(stub func(int, string) error) { fake.postCommentMutex.Lock() defer fake.postCommentMutex.Unlock() fake.PostCommentStub = stub } -func (fake *FakeGithub) PostCommentArgsForCall(i int) (string, string) { +func (fake *FakeGithub) PostCommentArgsForCall(i int) (int, string) { fake.postCommentMutex.RLock() defer fake.postCommentMutex.RUnlock() argsForCall := fake.postCommentArgsForCall[i] @@ -470,10 +470,10 @@ func (fake *FakeGithub) Invocations() map[string][][]interface{} { defer fake.invocationsMutex.RUnlock() fake.getChangedFilesMutex.RLock() defer fake.getChangedFilesMutex.RUnlock() + fake.getLatestOpenPullRequestMutex.RLock() + defer fake.getLatestOpenPullRequestMutex.RUnlock() fake.getPullRequestMutex.RLock() defer fake.getPullRequestMutex.RUnlock() - fake.listModifiedFilesMutex.RLock() - defer fake.listModifiedFilesMutex.RUnlock() fake.listOpenPullRequestsMutex.RLock() defer fake.listOpenPullRequestsMutex.RUnlock() fake.postCommentMutex.RLock() diff --git a/git.go b/git.go index 874b85f..5371f4e 100644 --- a/git.go +++ b/git.go @@ -127,7 +127,7 @@ func (g *GitClient) Fetch(uri string, prNumber int, depth int) error { return nil } -// CheckOut +// Checkout ... func (g *GitClient) Checkout(branch, sha string) error { if err := g.command("git", "checkout", "-b", branch, sha).Run(); err != nil { return fmt.Errorf("checkout failed: %s", err) diff --git a/github.go b/github.go index 7685910..3a33362 100644 --- a/github.go +++ b/github.go @@ -5,26 +5,28 @@ import ( "crypto/tls" "errors" "fmt" + "log" "net/http" "net/url" "os" "path" - "strconv" "strings" + "time" "github.com/google/go-github/github" "github.com/shurcooL/githubv4" + "github.com/telia-oss/github-pr-resource/pullrequest" "golang.org/x/oauth2" ) // Github for testing purposes. //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -o fakes/fake_github.go . Github type Github interface { - ListOpenPullRequests() ([]*PullRequest, error) - ListModifiedFiles(int) ([]string, error) - PostComment(string, string) error - GetPullRequest(string, string) (*PullRequest, error) - GetChangedFiles(string, string) ([]ChangedFileObject, error) + GetLatestOpenPullRequest() ([]pullrequest.PullRequest, error) + ListOpenPullRequests(prSince time.Time) ([]pullrequest.PullRequest, error) + PostComment(int, string) error + GetPullRequest(int, string) (pullrequest.PullRequest, error) + GetChangedFiles(int) ([]string, error) UpdateCommitStatus(string, string, string, string, string, string) error } @@ -43,23 +45,30 @@ func NewGithubClient(s *Source) (*GithubClient, error) { return nil, err } + ctx := context.TODO() + httpClient := http.Client{} + // Skip SSL verification for self-signed certificates // source: https://github.com/google/go-github/pull/598#issuecomment-333039238 - var ctx context.Context if s.SkipSSLVerification { - insecureClient := &http.Client{Transport: &http.Transport{ + log.Println("disabling SSL verification") + httpClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, } - ctx = context.WithValue(context.TODO(), oauth2.HTTPClient, insecureClient) - } else { - ctx = context.TODO() + ctx = context.WithValue(ctx, oauth2.HTTPClient, &httpClient) } client := oauth2.NewClient(ctx, oauth2.StaticTokenSource( &oauth2.Token{AccessToken: s.AccessToken}, )) + if s.PreviewSchema { + log.Println("attaching preview schema transport to client") + client.Transport = &PreviewSchemaTransport{ + oauthTransport: client.Transport, + } + } + var v3 *github.Client if s.V3Endpoint != "" { endpoint, err := url.Parse(s.V3Endpoint) @@ -96,102 +105,65 @@ func NewGithubClient(s *Source) (*GithubClient, error) { }, nil } -// ListOpenPullRequests gets the last commit on all open pull requests. -func (m *GithubClient) ListOpenPullRequests() ([]*PullRequest, error) { +// GetLatestOpenPullRequest gets the last commit on the latest open pull request +func (m *GithubClient) GetLatestOpenPullRequest() ([]pullrequest.PullRequest, error) { + return m.searchOpenPullRequests(time.Now().AddDate(-3, 0, 0), 3) +} + +// ListOpenPullRequests gets the last commit on all open pull requests +func (m *GithubClient) ListOpenPullRequests(since time.Time) ([]pullrequest.PullRequest, error) { + return m.searchOpenPullRequests(since, 100) +} + +func (m *GithubClient) searchOpenPullRequests(since time.Time, number int) ([]pullrequest.PullRequest, error) { + log.Println("building open pull requests query") + var query struct { - Repository struct { - PullRequests struct { - Edges []struct { - Node struct { - PullRequestObject - Commits struct { - Edges []struct { - Node struct { - Commit CommitObject - } - } - } `graphql:"commits(last:$commitsLast)"` - } - } - PageInfo struct { - EndCursor githubv4.String - HasNextPage bool + Search struct { + Edges []struct { + Node struct { + PullRequestObject `graphql:"... on PullRequest"` } - } `graphql:"pullRequests(first:$prFirst,states:$prStates,after:$prCursor)"` - } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` + } + PageInfo struct { + EndCursor githubv4.String + HasNextPage bool + } + } `graphql:"search(query:$q,type:ISSUE,last:$n,after:$c)"` } vars := map[string]interface{}{ - "repositoryOwner": githubv4.String(m.Owner), - "repositoryName": githubv4.String(m.Repository), - "prFirst": githubv4.Int(100), - "prStates": []githubv4.PullRequestState{githubv4.PullRequestStateOpen}, - "prCursor": (*githubv4.String)(nil), - "commitsLast": githubv4.Int(1), + "c": (*githubv4.String)(nil), + "s": githubv4.DateTime{Time: since}, + "n": githubv4.Int(number), + "q": githubv4.String( + fmt.Sprintf("is:pr is:open repo:%s/%s updated:>%s sort:updated", m.Owner, m.Repository, since.Format(time.RFC3339)), + ), } - var response []*PullRequest + var response []pullrequest.PullRequest for { if err := m.V4.Query(context.TODO(), &query, vars); err != nil { return nil, err } - for _, p := range query.Repository.PullRequests.Edges { - for _, c := range p.Node.Commits.Edges { - response = append(response, &PullRequest{ - PullRequestObject: p.Node.PullRequestObject, - Tip: c.Node.Commit, - }) - } + for _, p := range query.Search.Edges { + response = append(response, PullRequestFactory(p.Node.PullRequestObject)) } - if !query.Repository.PullRequests.PageInfo.HasNextPage { + if number < 100 || !query.Search.PageInfo.HasNextPage { break } - vars["prCursor"] = query.Repository.PullRequests.PageInfo.EndCursor + vars["c"] = query.Search.PageInfo.EndCursor } return response, nil } -// ListModifiedFiles in a pull request (not supported by V4 API). -func (m *GithubClient) ListModifiedFiles(prNumber int) ([]string, error) { - var files []string - - opt := &github.ListOptions{ - PerPage: 100, - } - for { - result, response, err := m.V3.PullRequests.ListFiles( - context.TODO(), - m.Owner, - m.Repository, - prNumber, - opt, - ) - if err != nil { - return nil, err - } - for _, f := range result { - files = append(files, *f.Filename) - } - if response.NextPage == 0 { - break - } - opt.Page = response.NextPage - } - return files, nil -} - // PostComment to a pull request or issue. -func (m *GithubClient) PostComment(prNumber, comment string) error { - pr, err := strconv.Atoi(prNumber) - if err != nil { - return fmt.Errorf("failed to convert pull request number to int: %s", err) - } - - _, _, err = m.V3.Issues.CreateComment( +func (m *GithubClient) PostComment(number int, comment string) error { + _, _, err := m.V3.Issues.CreateComment( context.TODO(), m.Owner, m.Repository, - pr, + number, &github.IssueComment{ Body: github.String(comment), }, @@ -200,13 +172,8 @@ func (m *GithubClient) PostComment(prNumber, comment string) error { } // GetChangedFiles ... -func (m *GithubClient) GetChangedFiles(prNumber string, commitRef string) ([]ChangedFileObject, error) { - pr, err := strconv.Atoi(prNumber) - if err != nil { - return nil, fmt.Errorf("failed to convert pull request number to int: %s", err) - } - - var cfo []ChangedFileObject +func (m *GithubClient) GetChangedFiles(number int) ([]string, error) { + log.Println("building pull request changed files query") var filequery struct { Repository struct { @@ -221,20 +188,20 @@ func (m *GithubClient) GetChangedFiles(prNumber string, commitRef string) ([]Cha EndCursor githubv4.String HasNextPage bool } `graphql:"pageInfo"` - } `graphql:"files(first:$changedFilesFirst, after: $changedFilesEndCursor)"` - } `graphql:"pullRequest(number:$prNumber)"` - } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` + } `graphql:"files(first:100, after: $c)"` + } `graphql:"pullRequest(number:$n)"` + } `graphql:"repository(owner:$owner,name:$name)"` } - offset := "" + files := []string{} + cursor := "" for { vars := map[string]interface{}{ - "repositoryOwner": githubv4.String(m.Owner), - "repositoryName": githubv4.String(m.Repository), - "prNumber": githubv4.Int(pr), - "changedFilesFirst": githubv4.Int(100), - "changedFilesEndCursor": githubv4.String(offset), + "owner": githubv4.String(m.Owner), + "name": githubv4.String(m.Repository), + "n": githubv4.Int(number), + "c": githubv4.String(cursor), } if err := m.V4.Query(context.TODO(), &filequery, vars); err != nil { @@ -242,25 +209,22 @@ func (m *GithubClient) GetChangedFiles(prNumber string, commitRef string) ([]Cha } for _, f := range filequery.Repository.PullRequest.Files.Edges { - cfo = append(cfo, ChangedFileObject{Path: f.Node.Path}) + files = append(files, f.Node.Path) } if !filequery.Repository.PullRequest.Files.PageInfo.HasNextPage { break } - offset = string(filequery.Repository.PullRequest.Files.PageInfo.EndCursor) + cursor = string(filequery.Repository.PullRequest.Files.PageInfo.EndCursor) } - return cfo, nil + return files, nil } // GetPullRequest ... -func (m *GithubClient) GetPullRequest(prNumber, commitRef string) (*PullRequest, error) { - pr, err := strconv.Atoi(prNumber) - if err != nil { - return nil, fmt.Errorf("failed to convert pull request number to int: %s", err) - } +func (m *GithubClient) GetPullRequest(number int, commitRef string) (pullrequest.PullRequest, error) { + log.Println("building pull request query") var query struct { Repository struct { @@ -272,35 +236,35 @@ func (m *GithubClient) GetPullRequest(prNumber, commitRef string) (*PullRequest, Commit CommitObject } } - } `graphql:"commits(last:$commitsLast)"` - } `graphql:"pullRequest(number:$prNumber)"` - } `graphql:"repository(owner:$repositoryOwner,name:$repositoryName)"` + } `graphql:"commits(last:$last)"` + } `graphql:"pullRequest(number:$number)"` + } `graphql:"repository(owner:$owner,name:$name)"` } vars := map[string]interface{}{ - "repositoryOwner": githubv4.String(m.Owner), - "repositoryName": githubv4.String(m.Repository), - "prNumber": githubv4.Int(pr), - "commitsLast": githubv4.Int(100), + "s": githubv4.DateTime{Time: time.Now().AddDate(-1, 0, 0)}, + "owner": githubv4.String(m.Owner), + "name": githubv4.String(m.Repository), + "number": githubv4.Int(number), + "last": githubv4.Int(100), } // TODO: Pagination - in case someone pushes > 100 commits before the build has time to start :p if err := m.V4.Query(context.TODO(), &query, vars); err != nil { - return nil, err + return pullrequest.PullRequest{}, err } for _, c := range query.Repository.PullRequest.Commits.Edges { if c.Node.Commit.OID == commitRef { // Return as soon as we find the correct ref. - return &PullRequest{ - PullRequestObject: query.Repository.PullRequest.PullRequestObject, - Tip: c.Node.Commit, - }, nil + pull := PullRequestFactory(query.Repository.PullRequest.PullRequestObject) + pull.HeadRef = commitFactory(c.Node.Commit) + return pull, nil } } // Return an error if the commit was not found - return nil, fmt.Errorf("commit with ref '%s' does not exist", commitRef) + return pullrequest.PullRequest{}, fmt.Errorf("commit with ref '%s' does not exist", commitRef) } // UpdateCommitStatus for a given commit (not supported by V4 API). @@ -343,3 +307,84 @@ func parseRepository(s string) (string, string, error) { } return parts[0], parts[1], nil } + +// PullRequestFactory generates a PullRequest object from a PullRequestObject +func PullRequestFactory(p PullRequestObject) pullrequest.PullRequest { + events := make([]pullrequest.Event, 0) + comments := make([]pullrequest.Comment, 0) + commits := make([]pullrequest.Commit, 0) + + for _, i := range p.TimelineItems.Edges { + switch i.Node.Typename { + case pullrequest.BaseRefChangedEvent: + events = append(events, pullrequest.Event{ + Type: pullrequest.BaseRefChangedEvent, + CreatedAt: i.Node.BaseRefChangedEvent.CreatedAt.Time, + }) + case pullrequest.BaseRefForcePushedEvent: + events = append(events, pullrequest.Event{ + Type: pullrequest.BaseRefForcePushedEvent, + CreatedAt: i.Node.BaseRefForcePushedEvent.CreatedAt.Time, + }) + case pullrequest.HeadRefForcePushedEvent: + events = append(events, pullrequest.Event{ + Type: pullrequest.HeadRefForcePushedEvent, + CreatedAt: i.Node.HeadRefForcePushedEvent.CreatedAt.Time, + }) + case pullrequest.ReopenedEvent: + events = append(events, pullrequest.Event{ + Type: pullrequest.ReopenedEvent, + CreatedAt: i.Node.ReopenedEvent.CreatedAt.Time, + }) + case pullrequest.IssueComment: + comments = append(comments, pullrequest.Comment{ + CreatedAt: i.Node.IssueComment.CreatedAt.Time, + Body: i.Node.IssueComment.BodyText, + }) + case pullrequest.PullRequestCommit: + commits = append(commits, commitFactory(i.Node.PullRequestCommit.Commit)) + } + } + + return pullrequest.PullRequest{ + ID: p.ID, + Number: p.Number, + Title: p.Title, + URL: p.URL, + RepositoryURL: p.Repository.URL, + BaseRefName: p.BaseRefName, + HeadRefName: p.HeadRefName, + IsCrossRepository: p.IsCrossRepository, + CreatedAt: p.CreatedAt.Time, + UpdatedAt: p.UpdatedAt.Time, + HeadRef: commitFactory(p.HeadRef.Target.CommitObject), + Events: events, + Commits: commits, + Comments: comments, + } +} + +func commitFactory(c CommitObject) pullrequest.Commit { + return pullrequest.Commit{ + OID: c.OID, + AbbreviatedOID: c.AbbreviatedOID, + AuthoredDate: c.AuthoredDate.Time, + CommittedDate: c.CommittedDate.Time, + PushedDate: c.PushedDate.Time, + Message: c.Message, + Author: c.Author.User.Login, + } +} + +// PreviewSchemaTransport is used to access GraphQL schema's hidden behind an Accept header by GitHub +type PreviewSchemaTransport struct { + oauthTransport http.RoundTripper +} + +// RoundTrip appends the Accept header and then executes the parent RoundTrip Transport +func (t *PreviewSchemaTransport) RoundTrip(r *http.Request) (*http.Response, error) { + log.Println("setting accept header for timelineItems & files connections preview schemas") + r.Header.Add("Accept", "application/vnd.github.starfire-preview+json, application/vnd.github.ocelot-preview+json") + + return t.oauthTransport.RoundTrip(r) +} diff --git a/github_test.go b/github_test.go new file mode 100644 index 0000000..a53a051 --- /dev/null +++ b/github_test.go @@ -0,0 +1,44 @@ +package resource_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + resource "github.com/telia-oss/github-pr-resource" +) + +func TestNewGithubClient(t *testing.T) { + tests := []struct { + description string + source resource.Source + expect struct { + owner string + repository string + } + }{ + { + description: "owner & repo set properly", + source: resource.Source{ + Repository: "itsdalmo/test-repository", + AccessToken: "oauthtoken", + }, + expect: struct { + owner string + repository string + }{ + owner: "itsdalmo", + repository: "test-repository", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + client, err := resource.NewGithubClient(&tc.source) + require.NoError(t, err) + assert.Equal(t, tc.expect.owner, client.Owner) + assert.Equal(t, tc.expect.repository, client.Repository) + }) + } +} diff --git a/go.mod b/go.mod index cb07d7a..6549464 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,17 @@ module github.com/telia-oss/github-pr-resource require ( + github.com/gobwas/glob v0.2.3 github.com/google/go-github v17.0.0+incompatible github.com/google/go-querystring v1.0.0 // indirect - github.com/maxbrunsfeld/counterfeiter/v6 v6.1.2 + github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2 github.com/shurcooL/githubv4 v0.0.0-20180925043049-51d7b505e2e9 github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e // indirect github.com/shurcooL/graphql v0.0.0-20180924043259-e4a3a37e6d42 // indirect github.com/stretchr/testify v1.3.0 + golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107 + golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa // indirect + golang.org/x/tools v0.0.0-20190806215303-88ddfcebc769 // indirect google.golang.org/appengine v1.2.0 // indirect ) diff --git a/go.sum b/go.sum index 667cc00..7c67ee4 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,12 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= @@ -12,10 +16,17 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/joefitzgerald/rainbow-reporter v0.1.0 h1:AuMG652zjdzI0YCCnXAqATtRBpGXMcAnrajcaTrSeuo= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= -github.com/maxbrunsfeld/counterfeiter/v6 v6.1.2 h1:KzrplGSxjzQiuHdnmLEyYaf8QCZNbt5gPntYY3ytKto= -github.com/maxbrunsfeld/counterfeiter/v6 v6.1.2/go.mod h1:we7LhkAAsf9qlEeywKZBLbRAnh6enCLYvwWT44w5xjI= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2 h1:g+4J5sZg6osfvEfkRZxJ1em0VT95/UOZgi/l7zi1/oE= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -31,12 +42,15 @@ github.com/shurcooL/graphql v0.0.0-20180924043259-e4a3a37e6d42/go.mod h1:AuYgA5K github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107 h1:63fpDttzclb8owmRoxSaFNbnT1CG25L0Yvnhh9lU1SE= golang.org/x/oauth2 v0.0.0-20181031022657-8527f56f7107/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= @@ -45,21 +59,31 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEha golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s= -golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190601110225-0abef6e9ecb8 h1:KFgOV120pDm8h0MBnt26wwMmwdhSXE+K+G9jg1ZjxbE= -golang.org/x/tools v0.0.0-20190601110225-0abef6e9ecb8/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db h1:9hRk1xeL9LTT3yX/941DqeBz87XgHAQuj+TbimYJuiw= +golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190806215303-88ddfcebc769 h1:D/+0wZ7qKh5vQqpbxJGPnaMv1tuCCKmn6heUpPt3FOk= +golang.org/x/tools v0.0.0-20190806215303-88ddfcebc769/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.2.0 h1:S0iUepdCWODXRvtE+gcRDd15L+k+k1AiHlMiMjefH24= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/in.go b/in.go index c8ce9d1..ad4fa00 100644 --- a/in.go +++ b/in.go @@ -24,7 +24,7 @@ func Get(request GetRequest, github Github, git Git, outputDir string) (*GetResp if err := git.Init(pull.BaseRefName); err != nil { return nil, err } - if err := git.Pull(pull.Repository.URL, pull.BaseRefName, request.Params.GitDepth); err != nil { + if err := git.Pull(pull.RepositoryURL, pull.BaseRefName, request.Params.GitDepth); err != nil { return nil, err } @@ -35,21 +35,21 @@ func Get(request GetRequest, github Github, git Git, outputDir string) (*GetResp } // Fetch the PR and merge the specified commit into the base - if err := git.Fetch(pull.Repository.URL, pull.Number, request.Params.GitDepth); err != nil { + if err := git.Fetch(pull.RepositoryURL, pull.Number, request.Params.GitDepth); err != nil { return nil, err } switch tool := request.Params.IntegrationTool; tool { case "rebase": - if err := git.Rebase(pull.BaseRefName, pull.Tip.OID); err != nil { + if err := git.Rebase(pull.BaseRefName, pull.HeadRef.OID); err != nil { return nil, err } case "merge", "": - if err := git.Merge(pull.Tip.OID); err != nil { + if err := git.Merge(pull.HeadRef.OID); err != nil { return nil, err } case "checkout": - if err := git.Checkout(pull.HeadRefName, pull.Tip.OID); err != nil { + if err := git.Checkout(pull.HeadRefName, pull.HeadRef.OID); err != nil { return nil, err } default: @@ -67,18 +67,19 @@ func Get(request GetRequest, github Github, git Git, outputDir string) (*GetResp metadata.Add("pr", strconv.Itoa(pull.Number)) metadata.Add("url", pull.URL) metadata.Add("head_name", pull.HeadRefName) - metadata.Add("head_sha", pull.Tip.OID) + metadata.Add("head_sha", pull.HeadRef.OID) + metadata.Add("head_short_sha", pull.HeadRef.AbbreviatedOID) metadata.Add("base_name", pull.BaseRefName) metadata.Add("base_sha", baseSHA) - metadata.Add("message", pull.Tip.Message) - metadata.Add("author", pull.Tip.Author.User.Login) + metadata.Add("message", pull.HeadRef.Message) + metadata.Add("author", pull.HeadRef.Author) // Write version and metadata for reuse in PUT path := filepath.Join(outputDir, ".git", "resource") if err := os.MkdirAll(path, os.ModePerm); err != nil { return nil, fmt.Errorf("failed to create output directory: %s", err) } - b, err := json.Marshal(request.Version) + b, err := json.Marshal(&request.Version) if err != nil { return nil, fmt.Errorf("failed to marshal version: %s", err) } @@ -103,7 +104,7 @@ func Get(request GetRequest, github Github, git Git, outputDir string) (*GetResp } if request.Params.ListChangedFiles { - cfol, err := github.GetChangedFiles(request.Version.PR, request.Version.Commit) + cfol, err := github.GetChangedFiles(request.Version.PR) if err != nil { return nil, fmt.Errorf("failed to fetch list of changed files: %s", err) } @@ -111,7 +112,7 @@ func Get(request GetRequest, github Github, git Git, outputDir string) (*GetResp var fl []byte for _, v := range cfol { - fl = append(fl, []byte(v.Path+"\n")...) + fl = append(fl, []byte(v+"\n")...) } // Create List with changed files diff --git a/in_test.go b/in_test.go index 7780799..c8df14b 100644 --- a/in_test.go +++ b/in_test.go @@ -1,18 +1,15 @@ package resource_test import ( - "fmt" - "io/ioutil" "os" "path/filepath" - "strconv" "testing" "time" - "github.com/shurcooL/githubv4" "github.com/stretchr/testify/assert" resource "github.com/telia-oss/github-pr-resource" "github.com/telia-oss/github-pr-resource/fakes" + "github.com/telia-oss/github-pr-resource/pullrequest" ) func TestGet(t *testing.T) { @@ -22,10 +19,10 @@ func TestGet(t *testing.T) { source resource.Source version resource.Version parameters resource.GetParameters - pullRequest *resource.PullRequest + pullRequest pullrequest.PullRequest versionString string metadataString string - files []resource.ChangedFileObject + files []string filesString string }{ { @@ -35,14 +32,14 @@ func TestGet(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.GetParameters{}, - pullRequest: createTestPR(1, "master", false, false), - versionString: `{"pr":"pr1","commit":"commit1","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, + pullRequest: createTestPR(1, "master", false, false, false, false), + versionString: `{"pr":"1","commit":"commit1","updated":"0001-01-01T00:00:00Z"}`, + metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"head_short_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, }, { description: "get supports unlocking with git crypt", @@ -52,14 +49,14 @@ func TestGet(t *testing.T) { GitCryptKey: "gitcryptkey", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.GetParameters{}, - pullRequest: createTestPR(1, "master", false, false), - versionString: `{"pr":"pr1","commit":"commit1","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, + pullRequest: createTestPR(1, "master", false, false, false, false), + versionString: `{"pr":"1","commit":"commit1","updated":"0001-01-01T00:00:00Z"}`, + metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"head_short_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, }, { description: "get supports rebasing", @@ -68,16 +65,16 @@ func TestGet(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.GetParameters{ IntegrationTool: "rebase", }, - pullRequest: createTestPR(1, "master", false, false), - versionString: `{"pr":"pr1","commit":"commit1","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, + pullRequest: createTestPR(1, "master", false, false, false, false), + versionString: `{"pr":"1","commit":"commit1","updated":"0001-01-01T00:00:00Z"}`, + metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"head_short_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, }, { description: "get supports checkout", @@ -86,16 +83,16 @@ func TestGet(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.GetParameters{ IntegrationTool: "checkout", }, - pullRequest: createTestPR(1, "master", false, false), - versionString: `{"pr":"pr1","commit":"commit1","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, + pullRequest: createTestPR(1, "master", false, false, false, false), + versionString: `{"pr":"1","commit":"commit1","updated":"0001-01-01T00:00:00Z"}`, + metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"head_short_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, }, { description: "get supports git_depth", @@ -104,16 +101,16 @@ func TestGet(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.GetParameters{ GitDepth: 2, }, - pullRequest: createTestPR(1, "master", false, false), - versionString: `{"pr":"pr1","commit":"commit1","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, + pullRequest: createTestPR(1, "master", false, false, false, false), + versionString: `{"pr":"1","commit":"commit1","updated":"0001-01-01T00:00:00Z"}`, + metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"head_short_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, }, { description: "get supports list_changed_files", @@ -122,24 +119,17 @@ func TestGet(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.GetParameters{ ListChangedFiles: true, }, - pullRequest: createTestPR(1, "master", false, false), - files: []resource.ChangedFileObject{ - { - Path: "README.md", - }, - { - Path: "Other.md", - }, - }, - versionString: `{"pr":"pr1","commit":"commit1","committed":"0001-01-01T00:00:00Z"}`, - metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, + pullRequest: createTestPR(1, "master", false, false, false, false), + files: []string{"README.md", "Other.md"}, + versionString: `{"pr":"1","commit":"commit1","updated":"0001-01-01T00:00:00Z"}`, + metadataString: `[{"name":"pr","value":"1"},{"name":"url","value":"pr1 url"},{"name":"head_name","value":"pr1"},{"name":"head_sha","value":"oid1"},{"name":"head_short_sha","value":"oid1"},{"name":"base_name","value":"master"},{"name":"base_sha","value":"sha"},{"name":"message","value":"commit message1"},{"name":"author","value":"login1"}]`, filesString: "README.md\nOther.md\n", }, } @@ -175,14 +165,15 @@ func TestGet(t *testing.T) { // Verify individual files files := map[string]string{ - "pr": "1", - "url": "pr1 url", - "head_name": "pr1", - "head_sha": "oid1", - "base_name": "master", - "base_sha": "sha", - "message": "commit message1", - "author": "login1", + "pr": "1", + "url": "pr1 url", + "head_name": "pr1", + "head_sha": "oid1", + "head_short_sha": "oid1", + "base_name": "master", + "base_sha": "sha", + "message": "commit message1", + "author": "login1", } for filename, expected := range files { @@ -211,7 +202,7 @@ func TestGet(t *testing.T) { if assert.Equal(t, 1, git.PullCallCount()) { url, base, depth := git.PullArgsForCall(0) - assert.Equal(t, tc.pullRequest.Repository.URL, url) + assert.Equal(t, tc.pullRequest.RepositoryURL, url) assert.Equal(t, tc.pullRequest.BaseRefName, base) assert.Equal(t, tc.parameters.GitDepth, depth) } @@ -223,7 +214,7 @@ func TestGet(t *testing.T) { if assert.Equal(t, 1, git.FetchCallCount()) { url, pr, depth := git.FetchArgsForCall(0) - assert.Equal(t, tc.pullRequest.Repository.URL, url) + assert.Equal(t, tc.pullRequest.RepositoryURL, url) assert.Equal(t, tc.pullRequest.Number, pr) assert.Equal(t, tc.parameters.GitDepth, depth) } @@ -233,18 +224,18 @@ func TestGet(t *testing.T) { if assert.Equal(t, 1, git.RebaseCallCount()) { branch, tip := git.RebaseArgsForCall(0) assert.Equal(t, tc.pullRequest.BaseRefName, branch) - assert.Equal(t, tc.pullRequest.Tip.OID, tip) + assert.Equal(t, tc.pullRequest.HeadRef.OID, tip) } case "checkout": if assert.Equal(t, 1, git.CheckoutCallCount()) { branch, sha := git.CheckoutArgsForCall(0) assert.Equal(t, tc.pullRequest.HeadRefName, branch) - assert.Equal(t, tc.pullRequest.Tip.OID, sha) + assert.Equal(t, tc.pullRequest.HeadRef.OID, sha) } default: if assert.Equal(t, 1, git.MergeCallCount()) { tip := git.MergeArgsForCall(0) - assert.Equal(t, tc.pullRequest.Tip.OID, tip) + assert.Equal(t, tc.pullRequest.HeadRef.OID, tip) } } if tc.source.GitCryptKey != "" { @@ -272,9 +263,9 @@ func TestGetSkipDownload(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.GetParameters{SkipDownload: true}, }, @@ -297,54 +288,3 @@ func TestGetSkipDownload(t *testing.T) { }) } } - -func createTestPR(count int, baseName string, skipCI bool, isCrossRepo bool) *resource.PullRequest { - n := strconv.Itoa(count) - d := time.Now().AddDate(0, 0, -count) - m := fmt.Sprintf("commit message%s", n) - if skipCI { - m = "[skip ci]" + m - } - - return &resource.PullRequest{ - PullRequestObject: resource.PullRequestObject{ - ID: fmt.Sprintf("pr%s", n), - Number: count, - Title: fmt.Sprintf("pr%s title", n), - URL: fmt.Sprintf("pr%s url", n), - BaseRefName: baseName, - HeadRefName: fmt.Sprintf("pr%s", n), - Repository: struct{ URL string }{ - URL: fmt.Sprintf("repo%s url", n), - }, - IsCrossRepository: isCrossRepo, - }, - Tip: resource.CommitObject{ - ID: fmt.Sprintf("commit%s", n), - OID: fmt.Sprintf("oid%s", n), - CommittedDate: githubv4.DateTime{Time: d}, - Message: m, - Author: struct{ User struct{ Login string } }{ - User: struct{ Login string }{ - Login: fmt.Sprintf("login%s", n), - }, - }, - }, - } -} - -func createTestDirectory(t *testing.T) string { - dir, err := ioutil.TempDir("", "github-pr-resource") - if err != nil { - t.Fatalf("failed to create temporary directory") - } - return dir -} - -func readTestFile(t *testing.T, path string) string { - b, err := ioutil.ReadFile(path) - if err != nil { - t.Fatalf("failed to read: %s: %s", path, err) - } - return string(b) -} diff --git a/log/dir.go b/log/dir.go new file mode 100644 index 0000000..5bbc089 --- /dev/null +++ b/log/dir.go @@ -0,0 +1,7 @@ +// +build !windows + +package log + +const ( + defaultDir = "/tmp" +) diff --git a/log/dir_windows.go b/log/dir_windows.go new file mode 100644 index 0000000..a8a3f18 --- /dev/null +++ b/log/dir_windows.go @@ -0,0 +1,5 @@ +package log + +const ( + defaultDir = "." +) diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..4c6d00a --- /dev/null +++ b/log/log.go @@ -0,0 +1,57 @@ +package log + +import ( + "fmt" + "io/ioutil" + "log" + "os" +) + +var ( + dir string + f *os.File + debug bool +) + +func init() { + dir = defaultDir + if os.Getenv("LOG_DIRECTORY") != "" { + dir = os.Getenv("LOG_DIRECTORY") + } + + if os.Getenv("LOG_DEBUG") != "" || true { + debug = true + } + + f, err := os.OpenFile(fmt.Sprintf("%s/github-pr-resource.log", dir), os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + log.Fatalf("error opening file: %v", err) + } + + log.SetFlags(log.Ldate | log.Ltime | log.Llongfile) + log.SetOutput(f) +} + +// WriteStdin will write the contents of stdin to the log, then return the contents if env var `LOG_DEBUG` != "" +func WriteStdin() []byte { + input, err := ioutil.ReadAll(os.Stdin) + if err != nil { + log.Fatal(err) + } + + if debug { + Write(string(input)) + } + + return input +} + +// Write writes a message to a log file in `/tmp` +func Write(msg string) { + log.Println(msg) +} + +// Close the *os.File connection for the logger +func Close() { + f.Close() +} diff --git a/models.go b/models.go index d913807..cc620a3 100644 --- a/models.go +++ b/models.go @@ -1,11 +1,13 @@ package resource import ( + "encoding/json" "errors" "strconv" "time" "github.com/shurcooL/githubv4" + "github.com/telia-oss/github-pr-resource/pullrequest" ) // Source represents the configuration for the resource. @@ -14,29 +16,26 @@ type Source struct { AccessToken string `json:"access_token"` V3Endpoint string `json:"v3_endpoint"` V4Endpoint string `json:"v4_endpoint"` - Paths []string `json:"paths"` - IgnorePaths []string `json:"ignore_paths"` - DisableCISkip bool `json:"disable_ci_skip"` - SkipSSLVerification bool `json:"skip_ssl_verification"` - DisableForks bool `json:"disable_forks"` - GitCryptKey string `json:"git_crypt_key"` - BaseBranch string `json:"base_branch"` + Paths []string `json:"paths,omitempty"` + IgnorePaths []string `json:"ignore_paths,omitempty"` + DisableCISkip bool `json:"disable_ci_skip,omitempty"` + SkipSSLVerification bool `json:"skip_ssl_verification,omitempty"` + DisableForks bool `json:"disable_forks,omitempty"` + GitCryptKey string `json:"git_crypt_key,omitempty"` + BaseBranch string `json:"base_branch,omitempty"` + PreviewSchema bool `json:"preview_schema,omitempty"` } // Validate the source configuration. func (s *Source) Validate() error { - if s.AccessToken == "" { - return errors.New("access_token must be set") + if s.AccessToken == "" || s.Repository == "" { + return errors.New("access_token & repository are required") } - if s.Repository == "" { - return errors.New("repository must be set") - } - if s.V3Endpoint != "" && s.V4Endpoint == "" { - return errors.New("v4_endpoint must be set together with v3_endpoint") - } - if s.V4Endpoint != "" && s.V3Endpoint == "" { - return errors.New("v3_endpoint must be set together with v4_endpoint") + + if len(s.V3Endpoint)+len(s.V4Endpoint) > 0 && (s.V3Endpoint == "" || s.V4Endpoint == "") { + return errors.New("both v3_endpoint & v4_endpoint endpoints are required for GitHub Enterprise") } + return nil } @@ -56,49 +55,124 @@ type MetadataField struct { // Version communicated with Concourse. type Version struct { - PR string `json:"pr"` - Commit string `json:"commit"` - CommittedDate time.Time `json:"committed,omitempty"` + PR int `json:"pr"` + Commit string `json:"commit"` + UpdatedDate time.Time `json:"updated"` } -// NewVersion constructs a new Version. -func NewVersion(p *PullRequest) Version { - return Version{ - PR: strconv.Itoa(p.Number), - Commit: p.Tip.OID, - CommittedDate: p.Tip.CommittedDate.Time, +// MarshalJSON custom marshaller to convert PR number +func (v *Version) MarshalJSON() ([]byte, error) { + type Alias Version + return json.Marshal(&struct { + PR string `json:"pr"` + *Alias + }{ + PR: strconv.Itoa(v.PR), + Alias: (*Alias)(v), + }) +} + +// UnmarshalJSON custom unmarshaller to convert PR number +func (v *Version) UnmarshalJSON(data []byte) error { + type Alias Version + aux := struct { + PR string `json:"pr"` + *Alias + }{ + Alias: (*Alias)(v), + } + + err := json.Unmarshal(data, &aux) + if err != nil { + return err } + + if aux.PR != "" { + v.PR, err = strconv.Atoi(aux.PR) + if err != nil { + return err + } + } + + return nil } -// PullRequest represents a pull request and includes the tip (commit). -type PullRequest struct { - PullRequestObject - Tip CommitObject +// NewVersion constructs a new Version +func NewVersion(p pullrequest.PullRequest) Version { + return Version{ + PR: p.Number, + Commit: p.HeadRef.OID, + UpdatedDate: p.UpdatedAt, + } } // PullRequestObject represents the GraphQL commit node. // https://developer.github.com/v4/object/pullrequest/ type PullRequestObject struct { - ID string - Number int - Title string - URL string - BaseRefName string - HeadRefName string - Repository struct { + ID string + Number int + Title string + URL string + BaseRefName string + HeadRefName string + IsCrossRepository bool + CreatedAt githubv4.DateTime + UpdatedAt githubv4.DateTime + HeadRef struct { + ID string + Name string + Target struct { + CommitObject `graphql:"... on Commit"` + } + } + Repository struct { URL string } - IsCrossRepository bool + TimelineItems struct { + Edges []struct { + Node struct { + Typename string `graphql:"__typename"` + BaseRefChangedEvent struct { + ID string + CreatedAt githubv4.DateTime + } `graphql:"... on BaseRefChangedEvent"` + BaseRefForcePushedEvent struct { + ID string + CreatedAt githubv4.DateTime + } `graphql:"... on BaseRefForcePushedEvent"` + HeadRefForcePushedEvent struct { + ID string + CreatedAt githubv4.DateTime + } `graphql:"... on HeadRefForcePushedEvent"` + IssueComment struct { + ID string + CreatedAt githubv4.DateTime + BodyText string + } `graphql:"... on IssueComment"` + ReopenedEvent struct { + ID string + CreatedAt githubv4.DateTime + } `graphql:"... on ReopenedEvent"` + PullRequestCommit struct { + ID string + Commit CommitObject + } `graphql:"... on PullRequestCommit"` + } + } + } `graphql:"timelineItems(last:100,since:$s)"` } // CommitObject represents the GraphQL commit node. // https://developer.github.com/v4/object/commit/ type CommitObject struct { - ID string - OID string - CommittedDate githubv4.DateTime - Message string - Author struct { + ID string + OID string + AbbreviatedOID string + AuthoredDate githubv4.DateTime + CommittedDate githubv4.DateTime + PushedDate githubv4.DateTime + Message string + Author struct { User struct { Login string } diff --git a/models_test.go b/models_test.go new file mode 100644 index 0000000..6c88b91 --- /dev/null +++ b/models_test.go @@ -0,0 +1,118 @@ +package resource_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + resource "github.com/telia-oss/github-pr-resource" +) + +func TestUnmarshalJSON(t *testing.T) { + tests := []struct { + description string + json []byte + request resource.CheckRequest + }{ + { + description: "simple", + json: []byte(`{"source":{"access_token":"XXXXX","paths":["test/*","ci/*","README*"],"preview_schema":true,"repository":"digitalocean/github-pr-resource","v3_endpoint":"https://github.com/api/v3/", "v4_endpoint":"https://github.com/api/graphql"},"version":null}`), + request: resource.CheckRequest{ + Source: resource.Source{ + AccessToken: "XXXXX", + Repository: "digitalocean/github-pr-resource", + PreviewSchema: true, + Paths: []string{"test/*", "ci/*", "README*"}, + V3Endpoint: "https://github.com/api/v3/", + V4Endpoint: "https://github.com/api/graphql", + }, + Version: resource.Version{}, + }, + }, + { + description: "simple with version", + json: []byte(`{"source":{"access_token":"XXXXX","paths":["test/*","ci/*","README*"],"preview_schema":true,"repository":"digitalocean/github-pr-resource","v3_endpoint":"https://github.com/api/v3/", "v4_endpoint":"https://github.com/api/graphql"},"version":{"pr":"1","commit":"a4afe32","updated":"2019-08-20T00:14:16Z"}}`), + request: resource.CheckRequest{ + Source: resource.Source{ + AccessToken: "XXXXX", + Repository: "digitalocean/github-pr-resource", + PreviewSchema: true, + Paths: []string{"test/*", "ci/*", "README*"}, + V3Endpoint: "https://github.com/api/v3/", + V4Endpoint: "https://github.com/api/graphql", + }, + Version: resource.Version{ + PR: 1, + Commit: "a4afe32", + UpdatedDate: time.Date(2019, time.August, 20, 0, 14, 16, 0, time.UTC), + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + var output resource.CheckRequest + + err := json.Unmarshal(tc.json, &output) + if assert.NoError(t, err) { + assert.Equal(t, tc.request.Source, output.Source) + assert.Equal(t, tc.request.Version, output.Version) + + err = output.Source.Validate() + assert.NoError(t, err) + } + }) + } +} + +func TestMarshalJSON(t *testing.T) { + tests := []struct { + description string + json []byte + versions []resource.Version + }{ + { + description: "empty", + json: []byte(`[]`), + versions: []resource.Version{}, + }, + { + description: "one version", + json: []byte(`[{"pr":"1","commit":"a4afe32","updated":"2019-08-20T00:14:16Z"}]`), + versions: []resource.Version{ + resource.Version{ + PR: 1, + Commit: "a4afe32", + UpdatedDate: time.Date(2019, time.August, 20, 0, 14, 16, 0, time.UTC), + }, + }, + }, + { + description: "many versions", + json: []byte(`[{"pr":"1","commit":"a4afe32","updated":"2019-08-20T00:14:16Z"},{"pr":"2","commit":"a4afe33","updated":"2020-08-20T00:14:16Z"}]`), + versions: []resource.Version{ + resource.Version{ + PR: 1, + Commit: "a4afe32", + UpdatedDate: time.Date(2019, time.August, 20, 0, 14, 16, 0, time.UTC), + }, + resource.Version{ + PR: 2, + Commit: "a4afe33", + UpdatedDate: time.Date(2020, time.August, 20, 0, 14, 16, 0, time.UTC), + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + output, err := json.Marshal(tc.versions) + if assert.NoError(t, err) { + assert.Equal(t, tc.json, output) + } + }) + } +} diff --git a/out.go b/out.go index 632d4ca..6aa99cf 100644 --- a/out.go +++ b/out.go @@ -38,7 +38,7 @@ func Put(request PutRequest, manager Github, inputDir string) (*PutResponse, err // Set status if specified if p := request.Params; p.Status != "" { - if err := manager.UpdateCommitStatus(version.Commit, p.BaseContext, p.Context, p.Status, os.ExpandEnv(p.TargetURL), p.Description); err != nil { + if err := manager.UpdateCommitStatus(version.Commit, p.BaseContext, os.ExpandEnv(p.Context), p.Status, os.ExpandEnv(p.TargetURL), p.Description); err != nil { return nil, fmt.Errorf("failed to set status: %s", err) } } diff --git a/out_test.go b/out_test.go index 56157e1..353d42b 100644 --- a/out_test.go +++ b/out_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" resource "github.com/telia-oss/github-pr-resource" "github.com/telia-oss/github-pr-resource/fakes" + "github.com/telia-oss/github-pr-resource/pullrequest" ) func TestPut(t *testing.T) { @@ -19,7 +20,7 @@ func TestPut(t *testing.T) { source resource.Source version resource.Version parameters resource.PutParameters - pullRequest *resource.PullRequest + pullRequest pullrequest.PullRequest }{ { description: "put with no parameters does nothing", @@ -28,12 +29,12 @@ func TestPut(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.PutParameters{}, - pullRequest: createTestPR(1, "master", false, false), + pullRequest: createTestPR(1, "master", false, false, false, false), }, { @@ -43,14 +44,14 @@ func TestPut(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.PutParameters{ Status: "success", }, - pullRequest: createTestPR(1, "master", false, false), + pullRequest: createTestPR(1, "master", false, false, false, false), }, { @@ -60,15 +61,15 @@ func TestPut(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.PutParameters{ Status: "failure", Context: "build", }, - pullRequest: createTestPR(1, "master", false, false), + pullRequest: createTestPR(1, "master", false, false, false, false), }, { @@ -78,16 +79,16 @@ func TestPut(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.PutParameters{ Status: "failure", BaseContext: "concourse-ci-custom", Context: "build", }, - pullRequest: createTestPR(1, "master", false, false), + pullRequest: createTestPR(1, "master", false, false, false, false), }, { @@ -97,15 +98,15 @@ func TestPut(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.PutParameters{ Status: "failure", TargetURL: "https://targeturl.com/concourse", }, - pullRequest: createTestPR(1, "master", false, false), + pullRequest: createTestPR(1, "master", false, false, false, false), }, { @@ -115,15 +116,15 @@ func TestPut(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.PutParameters{ Status: "failure", Description: "Concourse CI build", }, - pullRequest: createTestPR(1, "master", false, false), + pullRequest: createTestPR(1, "master", false, false, false, false), }, { @@ -133,14 +134,14 @@ func TestPut(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.PutParameters{ Comment: "comment", }, - pullRequest: createTestPR(1, "master", false, false), + pullRequest: createTestPR(1, "master", false, false, false, false), }, } @@ -207,7 +208,7 @@ func TestVariableSubstitution(t *testing.T) { parameters resource.PutParameters expectedComment string expectedTargetURL string - pullRequest *resource.PullRequest + pullRequest pullrequest.PullRequest }{ { @@ -217,15 +218,15 @@ func TestVariableSubstitution(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.PutParameters{ Comment: fmt.Sprintf("$%s", variableName), }, expectedComment: variableValue, - pullRequest: createTestPR(1, "master", false, false), + pullRequest: createTestPR(1, "master", false, false, false, false), }, { @@ -235,16 +236,16 @@ func TestVariableSubstitution(t *testing.T) { AccessToken: "oauthtoken", }, version: resource.Version{ - PR: "pr1", - Commit: "commit1", - CommittedDate: time.Time{}, + PR: 1, + Commit: "commit1", + UpdatedDate: time.Time{}, }, parameters: resource.PutParameters{ Status: "failure", TargetURL: fmt.Sprintf("%s$%s", variableURL, variableName), }, expectedTargetURL: fmt.Sprintf("%s%s", variableURL, variableValue), - pullRequest: createTestPR(1, "master", false, false), + pullRequest: createTestPR(1, "master", false, false, false, false), }, } diff --git a/pullrequest/filter.go b/pullrequest/filter.go new file mode 100644 index 0000000..36ca33b --- /dev/null +++ b/pullrequest/filter.go @@ -0,0 +1,199 @@ +package pullrequest + +import ( + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/gobwas/glob" +) + +// TimelineItem constants +const ( + BaseRefChangedEvent = "BaseRefChangedEvent" + BaseRefForcePushedEvent = "BaseRefForcePushedEvent" + HeadRefForcePushedEvent = "HeadRefForcePushedEvent" + IssueComment = "IssueComment" + PullRequestCommit = "PullRequestCommit" + ReopenedEvent = "ReopenedEvent" +) + +// Filter is a function that filters a slice of PRs, returning the filtered slice. +type Filter func(PullRequest) bool + +// SkipCI returns true if the PR title or HeadRef message contains [skip ci] & the feature is not disabled +func SkipCI(disabled bool) Filter { + return func(p PullRequest) bool { + if disabled { + return false + } + + re := regexp.MustCompile("(?i)\\[(ci skip|skip ci)\\]") + if re.MatchString(p.Title) || re.MatchString(p.HeadRef.Message) { + log.Println("skipCI: true") + return true + } + + return false + } +} + +// Fork returns true if the source DisableForks is true && the PR is from a fork +func Fork(disabled bool) Filter { + return func(p PullRequest) bool { + if disabled && p.IsCrossRepository { + log.Println("fork: true") + return true + } + + return false + } +} + +// BaseBranch returns true if the source BaseBranch is set & it does not match the PR +func BaseBranch(b string) Filter { + return func(p PullRequest) bool { + if b == "" { + return false + } + + if b != p.BaseRefName { + log.Println("baseBranch: true") + return true + } + + return false + } +} + +// Created returns true if the PR was created with no new commits +func Created() Filter { + return func(p PullRequest) bool { + if p.CreatedAt.Equal(p.UpdatedAt) { + log.Println("created: true") + return true + } + if p.CreatedAt.After(latest(p.HeadRef.AuthoredDate, p.HeadRef.CommittedDate, p.HeadRef.PushedDate)) { + log.Println("created: true") + return true + } + return false + } +} + +// BuildCI returns true if a comment containing [build ci] was added since the last check +func BuildCI() Filter { + return func(p PullRequest) bool { + for _, c := range p.Comments { + re := regexp.MustCompile("(?i)\\[(ci build|build ci)\\]") + if re.MatchString(c.Body) { + log.Println("buildCI: true") + return true + } + } + return false + } +} + +// NewCommits returns true if the PR has new commits since the input version.UpdatedDate +func NewCommits(v time.Time) Filter { + return func(p PullRequest) bool { + if v.IsZero() { + log.Println("new commits: true") + return true + } + + if latest(p.HeadRef.AuthoredDate, p.HeadRef.CommittedDate, p.HeadRef.PushedDate).After(v) { + log.Println("new commits: true") + return true + } + return false + } +} + +// BaseRefChanged returns true if the PR contains a BaseRefChangedEvent since the last check +func BaseRefChanged() Filter { + return filterEvent(BaseRefChangedEvent) +} + +// BaseRefForcePushed returns true if the PR contains a BaseRefForcePushedEvent since the last check +func BaseRefForcePushed() Filter { + return filterEvent(BaseRefForcePushedEvent) +} + +// HeadRefForcePushed returns true if the PR contains a HeadRefForcePushedEvent since the last check +func HeadRefForcePushed() Filter { + return filterEvent(HeadRefForcePushedEvent) +} + +// Reopened returns true if the PR contains a ReopenedEvent since the last check +func Reopened() Filter { + return filterEvent(ReopenedEvent) +} + +func filterEvent(eventType string) Filter { + return func(p PullRequest) bool { + for _, i := range p.Events { + log.Println("filter:", eventType, "item type:", i.Type) + if eventType != i.Type { + continue + } + + log.Println(eventType, ": true") + return true + } + return false + } +} + +// Patterns returns true if there is a pattern configured +func Patterns(patterns []string) Filter { + return func(p PullRequest) bool { + if len(patterns) > 0 { + return true + } + + return false + } +} + +// Files matches the PRs changed files against a set of glob patterns: +// (invert == false) returns true if patterns empty OR any of the changed files match the patterns +// (invert == true) returns true if all of the changed files match the patterns to ignore +func Files(patterns []string, invert bool) Filter { + return func(p PullRequest) bool { + matched := make([]int8, 0) + pattern := strings.Join(patterns[:], ",") + gc := glob.MustCompile(fmt.Sprintf("{%s}", pattern)) + for _, f := range p.Files { + log.Println("comparing patterns to changed file:", f) + if gc.Match(f) { + if !invert { + log.Println("paths: true") + return true + } + + matched = append(matched, 1) + } + } + + if invert && len(matched) == len(p.Files) { + log.Println("ignore paths: true") + return true + } + + return false + } +} + +func latest(times ...time.Time) time.Time { + var latest time.Time + for _, t := range times { + if t.After(latest) { + latest = t + } + } + return latest +} diff --git a/pullrequest/filter_test.go b/pullrequest/filter_test.go new file mode 100644 index 0000000..d305a7b --- /dev/null +++ b/pullrequest/filter_test.go @@ -0,0 +1,33 @@ +package pullrequest + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestLatest(t *testing.T) { + tests := []struct { + description string + input []time.Time + expect time.Time + }{ + { + description: "simple test w/3 input arguments", + input: []time.Time{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC), + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).AddDate(0, 1, 0), + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).AddDate(0, -1, 0), + }, + expect: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).AddDate(0, 1, 0), + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := latest(tc.input...) + assert.Equal(t, out, tc.expect) + }) + } +} diff --git a/pullrequest/functional_test.go b/pullrequest/functional_test.go new file mode 100644 index 0000000..812e7c7 --- /dev/null +++ b/pullrequest/functional_test.go @@ -0,0 +1,662 @@ +package pullrequest_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/telia-oss/github-pr-resource/pullrequest" +) + +// Test Filters + +func TestSkipCI(t *testing.T) { + tests := []struct { + description string + disabled bool + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match title v1", + disabled: false, + pull: pullrequest.PullRequest{ + Title: "WIP: Weeee... [skip ci]", + HeadRef: pullrequest.Commit{ + Message: "this is a test", + }, + }, + expect: true, + }, + { + description: "match title v2", + disabled: false, + pull: pullrequest.PullRequest{ + Title: "WIP: Weeee... [ci skip]", + HeadRef: pullrequest.Commit{ + Message: "this is a test", + }, + }, + expect: true, + }, + { + description: "match message v1", + disabled: false, + pull: pullrequest.PullRequest{ + Title: "WIP: Weeee...", + HeadRef: pullrequest.Commit{ + Message: "this is a test [skip ci]", + }, + }, + expect: true, + }, + { + description: "match message v2", + disabled: false, + pull: pullrequest.PullRequest{ + Title: "WIP: Weeee...", + HeadRef: pullrequest.Commit{ + Message: "this is a test [ci skip]", + }, + }, + expect: true, + }, + { + description: "match nothing", + disabled: false, + pull: pullrequest.PullRequest{ + Title: "WIP: Weeee... I should skip to the CI", + HeadRef: pullrequest.Commit{ + Message: "this is a test ci [ skip]", + }, + }, + expect: false, + }, + { + description: "match nothing disabled", + disabled: true, + pull: pullrequest.PullRequest{ + Title: "WIP: Weeee... [ci skip]", + HeadRef: pullrequest.Commit{ + Message: "this is a test [ci skip]", + }, + }, + expect: false, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.SkipCI(tc.disabled)(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} + +func TestFork(t *testing.T) { + tests := []struct { + description string + disabled bool + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match", + disabled: true, + pull: pullrequest.PullRequest{ + IsCrossRepository: true, + }, + expect: true, + }, + { + description: "no match", + disabled: false, + pull: pullrequest.PullRequest{ + IsCrossRepository: false, + }, + expect: false, + }, + { + description: "no match disabled", + disabled: true, + pull: pullrequest.PullRequest{ + IsCrossRepository: false, + }, + expect: false, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.Fork(tc.disabled)(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} + +func TestBaseBranch(t *testing.T) { + tests := []struct { + description string + branch string + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match set", + branch: "develop", + pull: pullrequest.PullRequest{ + BaseRefName: "master", + }, + expect: true, + }, + { + description: "no match not set", + branch: "", + pull: pullrequest.PullRequest{ + BaseRefName: "master", + }, + expect: false, + }, + { + description: "no match set", + branch: "master", + pull: pullrequest.PullRequest{ + BaseRefName: "master", + }, + expect: false, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.BaseBranch(tc.branch)(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} + +func TestCreated(t *testing.T) { + tests := []struct { + description string + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match Created==Updated", + pull: pullrequest.PullRequest{ + CreatedAt: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expect: true, + }, + { + description: "match Created after head commit", + pull: pullrequest.PullRequest{ + CreatedAt: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), + HeadRef: pullrequest.Commit{ + CommittedDate: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), + PushedDate: time.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC), + AuthoredDate: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), + }, + }, + expect: true, + }, + { + description: "no match", + pull: pullrequest.PullRequest{ + CreatedAt: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), + HeadRef: pullrequest.Commit{ + CommittedDate: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), + PushedDate: time.Date(2019, 2, 1, 0, 0, 0, 0, time.UTC), + AuthoredDate: time.Date(2019, 1, 2, 0, 0, 0, 0, time.UTC), + }, + }, + expect: false, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.Created()(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} + +func TestNewCommits(t *testing.T) { + tests := []struct { + description string + versionDate time.Time + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match empty version", + versionDate: time.Time{}, + pull: pullrequest.PullRequest{ + HeadRef: pullrequest.Commit{ + CommittedDate: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), + PushedDate: time.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC), + AuthoredDate: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), + }, + }, + expect: true, + }, + { + description: "no match version date now", + versionDate: time.Now(), + pull: pullrequest.PullRequest{ + HeadRef: pullrequest.Commit{ + CommittedDate: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), + PushedDate: time.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC), + AuthoredDate: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), + }, + }, + expect: false, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.NewCommits(tc.versionDate)(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} + +func TestBuildCI(t *testing.T) { + tests := []struct { + description string + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match v1", + pull: pullrequest.PullRequest{ + Comments: []pullrequest.Comment{ + pullrequest.Comment{Body: "weeee I don't want to build ci"}, + pullrequest.Comment{Body: "weeee I do want to [build ci]"}, + }, + }, + expect: true, + }, + { + description: "match v2", + pull: pullrequest.PullRequest{ + Comments: []pullrequest.Comment{ + pullrequest.Comment{Body: "weeee I don't want to build ci"}, + pullrequest.Comment{Body: "weeee I do want to [ci build]"}, + }, + }, + expect: true, + }, + { + description: "no match", + pull: pullrequest.PullRequest{ + Comments: []pullrequest.Comment{ + pullrequest.Comment{Body: "weeee I don't want to build ci"}, + }, + }, + expect: false, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.BuildCI()(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} + +func TestBaseRefChanged(t *testing.T) { + tests := []struct { + description string + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match single", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.BaseRefChangedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: true, + }, + { + description: "match many", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.BaseRefForcePushedEvent, + CreatedAt: time.Now(), + }, + pullrequest.Event{ + Type: pullrequest.BaseRefChangedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: true, + }, + { + description: "no match", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.BaseRefForcePushedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: false, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.BaseRefChanged()(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} + +func TestBaseRefForcePushed(t *testing.T) { + tests := []struct { + description string + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match single", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.BaseRefForcePushedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: true, + }, + { + description: "match many", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.HeadRefForcePushedEvent, + CreatedAt: time.Now(), + }, + pullrequest.Event{ + Type: pullrequest.HeadRefForcePushedEvent, + CreatedAt: time.Now(), + }, + pullrequest.Event{ + Type: pullrequest.BaseRefChangedEvent, + CreatedAt: time.Now(), + }, + pullrequest.Event{ + Type: pullrequest.BaseRefForcePushedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: true, + }, + { + description: "no match", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.BaseRefChangedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: false, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.BaseRefForcePushed()(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} + +func TestHeadRefForcePushed(t *testing.T) { + tests := []struct { + description string + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match single", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.HeadRefForcePushedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: true, + }, + { + description: "match many", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.HeadRefForcePushedEvent, + CreatedAt: time.Now(), + }, + pullrequest.Event{ + Type: pullrequest.HeadRefForcePushedEvent, + CreatedAt: time.Now(), + }, + pullrequest.Event{ + Type: pullrequest.BaseRefChangedEvent, + CreatedAt: time.Now(), + }, + pullrequest.Event{ + Type: pullrequest.BaseRefForcePushedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: true, + }, + { + description: "no match", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.BaseRefForcePushedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: false, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.HeadRefForcePushed()(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} + +func TestReopened(t *testing.T) { + tests := []struct { + description string + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match single", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.ReopenedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: true, + }, + { + description: "match many", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.HeadRefForcePushedEvent, + CreatedAt: time.Now(), + }, + pullrequest.Event{ + Type: pullrequest.HeadRefForcePushedEvent, + CreatedAt: time.Now(), + }, + pullrequest.Event{ + Type: pullrequest.ReopenedEvent, + CreatedAt: time.Now(), + }, + pullrequest.Event{ + Type: pullrequest.BaseRefForcePushedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: true, + }, + { + description: "no match", + pull: pullrequest.PullRequest{ + Events: []pullrequest.Event{ + pullrequest.Event{ + Type: pullrequest.HeadRefForcePushedEvent, + CreatedAt: time.Now(), + }, + }, + }, + expect: false, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.Reopened()(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} + +func TestFiles(t *testing.T) { + tests := []struct { + description string + patterns []string + invert bool + pull pullrequest.PullRequest + expect bool + }{ + { + description: "match txt files @ root level", + patterns: []string{"*.txt"}, + invert: false, + pull: pullrequest.PullRequest{ + Files: []string{ + "file1.txt", + }, + }, + expect: true, + }, + /* { + description: "no match txt files @ root level", + patterns: []string{"*.txt"}, + invert: false, + pull: pullrequest.PullRequest{ + Files: []string{ + "test/file2.txt", + }, + }, + expect: false, + },*/ + { + description: "match txt files at any level", + patterns: []string{"**/*.txt"}, + invert: false, + pull: pullrequest.PullRequest{ + Files: []string{ + "test/file2.txt", + "test/testing/file2.txt", + "test/testing/tested/file2.txt", + }, + }, + expect: true, + }, + { + description: "match any file in test dir", + patterns: []string{"test/*"}, + invert: false, + pull: pullrequest.PullRequest{ + Files: []string{ + "file1.txt", + "test/file2.txt", + }, + }, + expect: true, + }, + /* { + description: "no match any file in test dir", + patterns: []string{"test/*"}, + invert: false, + pull: pullrequest.PullRequest{ + Files: []string{ + "file1.txt", + "test/testing/file2.txt", + "test/testing/tested/file2.txt", + }, + }, + expect: false, + },*/ + { + description: "match any file recursively in test dir", + patterns: []string{"test/**"}, + invert: false, + pull: pullrequest.PullRequest{ + Files: []string{ + "file1.txt", + "test/testing/file2.txt", + "test/testing/tested/file2.txt", + }, + }, + expect: true, + }, + { + description: "match multiple files", + patterns: []string{"ci/dockerfiles/**/*", "ci/dockerfiles/*", "ci/tasks/build-image.yml", "ci/pipelines/images.yml"}, + invert: false, + pull: pullrequest.PullRequest{ + Files: []string{ + "ci/Makefile", + "ci/pipelines/images.yml", + "terraform/Makefile", + }, + }, + expect: true, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + out := pullrequest.Files(tc.patterns, tc.invert)(tc.pull) + assert.Equal(t, tc.expect, out) + }) + } +} diff --git a/pullrequest/pullrequest.go b/pullrequest/pullrequest.go new file mode 100644 index 0000000..c175cad --- /dev/null +++ b/pullrequest/pullrequest.go @@ -0,0 +1,45 @@ +package pullrequest + +import "time" + +// PullRequest represents a pull request +type PullRequest struct { + ID string + Number int + Title string + URL string + RepositoryURL string + BaseRefName string + HeadRefName string + IsCrossRepository bool + CreatedAt time.Time + UpdatedAt time.Time + HeadRef Commit + Events []Event + Comments []Comment + Commits []Commit + Files []string +} + +// Commit represents a commit +type Commit struct { + OID string + AbbreviatedOID string + AuthoredDate time.Time + CommittedDate time.Time + PushedDate time.Time + Message string + Author string +} + +// Event represents an event that has been recorded on the PR +type Event struct { + Type string + CreatedAt time.Time +} + +// Comment represents a comment on a PR +type Comment struct { + CreatedAt time.Time + Body string +} diff --git a/pullrequest/pullrequest_test.go b/pullrequest/pullrequest_test.go new file mode 100644 index 0000000..b6c1c32 --- /dev/null +++ b/pullrequest/pullrequest_test.go @@ -0,0 +1,3 @@ +package pullrequest_test + +import _ "github.com/telia-oss/github-pr-resource/log" diff --git a/resource_test.go b/resource_test.go new file mode 100644 index 0000000..20ce86c --- /dev/null +++ b/resource_test.go @@ -0,0 +1,90 @@ +package resource_test + +import ( + "fmt" + "io/ioutil" + "strconv" + "testing" + "time" + + "github.com/shurcooL/githubv4" + resource "github.com/telia-oss/github-pr-resource" + _ "github.com/telia-oss/github-pr-resource/log" + "github.com/telia-oss/github-pr-resource/pullrequest" +) + +func createTestPR(count int, baseName string, skipCI, isCrossRepo, created, nocommit bool) pullrequest.PullRequest { + n := strconv.Itoa(count) + u := time.Now().AddDate(0, 0, count) + + c := u + if !created { + c = time.Now().AddDate(0, 0, count-1) + } + + m := fmt.Sprintf("commit message%s", n) + if skipCI { + m = "[skip ci]" + m + } + + commit := resource.CommitObject{ + ID: fmt.Sprintf("commit%s", n), + OID: fmt.Sprintf("oid%s", n), + AbbreviatedOID: fmt.Sprintf("oid%s", n), + CommittedDate: githubv4.DateTime{Time: u}, + Message: m, + Author: struct{ User struct{ Login string } }{ + User: struct{ Login string }{ + Login: fmt.Sprintf("login%s", n), + }, + }, + } + if nocommit { + commit.ID = "" + } + + return resource.PullRequestFactory(resource.PullRequestObject{ + ID: fmt.Sprintf("pr%s", n), + Number: count, + Title: fmt.Sprintf("pr%s title", n), + URL: fmt.Sprintf("pr%s url", n), + BaseRefName: baseName, + HeadRefName: fmt.Sprintf("pr%s", n), + IsCrossRepository: isCrossRepo, + CreatedAt: githubv4.DateTime{Time: c}, + UpdatedAt: githubv4.DateTime{Time: u}, + HeadRef: struct { + ID string + Name string + Target struct { + resource.CommitObject `graphql:"... on Commit"` + } + }{ + ID: fmt.Sprintf("commit%s", n), + Name: fmt.Sprintf("pr%s", n), + Target: struct { + resource.CommitObject `graphql:"... on Commit"` + }{commit}, + }, + Repository: struct{ URL string }{ + URL: fmt.Sprintf("repo%s url", n), + }, + }, + ) +} + +func createTestDirectory(t *testing.T) string { + dir, err := ioutil.TempDir("", "github-pr-resource") + if err != nil { + t.Fatalf("failed to create temporary directory") + } + return dir +} + +func readTestFile(t *testing.T, path string) string { + b, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("failed to read: %s: %s", path, err) + } + return string(b) +}