diff --git a/Gopkg.lock b/Gopkg.lock index a8e7111b83f..56cd158ba10 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -382,18 +382,19 @@ version = "v0.3.7" [[projects]] - digest = "1:a8c65321b38b41bf6a74d08cb0941e039a9e4762230ba446d86b1c5d71772e38" + digest = "1:58d75a5b3febc0132cc506b092f9e13206970f2bfe7332e1267290ae91bcef2a" name = "github.com/jenkins-x/go-scm" packages = [ "scm", "scm/driver/fake", "scm/driver/github", + "scm/driver/gitlab", "scm/driver/internal/hmac", "scm/driver/internal/null", ] pruneopts = "NUT" - revision = "e94d536581a8e4d5197b9cde009c6fc0077ab6a5" - version = "v1.5.58" + revision = "641e9289bd84c4e14e1e5790d8a5531ddcc949fa" + version = "v1.5.61" [[projects]] digest = "1:1f2aebae7e7c856562355ec0198d8ca2fa222fb05e5b1b66632a1fce39631885" @@ -1365,6 +1366,7 @@ "github.com/jenkins-x/go-scm/scm", "github.com/jenkins-x/go-scm/scm/driver/fake", "github.com/jenkins-x/go-scm/scm/driver/github", + "github.com/jenkins-x/go-scm/scm/driver/gitlab", "github.com/knative/test-infra/tools/dep-collector", "github.com/mitchellh/go-homedir", "github.com/pkg/errors", diff --git a/cmd/pullrequest-init/github.go b/cmd/pullrequest-init/github.go deleted file mode 100644 index 3cefbaf56ed..00000000000 --- a/cmd/pullrequest-init/github.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "context" - "fmt" - "net/url" - "os" - "strconv" - "strings" - - "golang.org/x/oauth2" - - "github.com/jenkins-x/go-scm/scm/driver/github" - "go.uber.org/zap" -) - -func NewGitHubHandler(ctx context.Context, logger *zap.SugaredLogger, raw string) (*Handler, error) { - u, err := url.Parse(raw) - if err != nil { - return nil, err - } - split := strings.Split(u.Path, "/") - if len(split) < 5 { - return nil, fmt.Errorf("could not determine PR from URL: %v", raw) - } - owner, repo, pr := split[1], split[2], split[4] - prNumber, err := strconv.Atoi(pr) - if err != nil { - return nil, fmt.Errorf("error parsing PR number: %s", pr) - } - - client := github.NewDefault() - token := os.Getenv("GITHUB_TOKEN") - if token != "" { - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - ) - hc := oauth2.NewClient(ctx, ts) - client.Client = hc - } - ownerRepo := fmt.Sprintf("%s/%s", owner, repo) - return NewHandler(logger, client, ownerRepo, prNumber), nil -} diff --git a/cmd/pullrequest-init/github_test.go b/cmd/pullrequest-init/github_test.go deleted file mode 100644 index 42bbbb30fba..00000000000 --- a/cmd/pullrequest-init/github_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "context" - "testing" - - "go.uber.org/zap" - "go.uber.org/zap/zaptest" -) - -func TestNewGitHubHandler(t *testing.T) { - for _, url := range []string{ - "https://github.com/foo/bar/pull/1", - "https://github.tekton.dev/foo/bar/pull/1", - } { - t.Run(url, func(t *testing.T) { - h, err := NewGitHubHandler(context.Background(), zaptest.NewLogger(t, zaptest.WrapOptions(zap.AddCaller())).Sugar(), url) - if err != nil { - t.Fatalf("error creating GitHubHandler: %v", err) - } - if h.repo != repo { - t.Fatalf("error unexpected repo: %v", h.repo) - } - if h.prNum != prNum { - t.Fatalf("error unexpected PR number: %v", h.prNum) - } - }) - } -} diff --git a/cmd/pullrequest-init/main.go b/cmd/pullrequest-init/main.go index bdacb384b20..9758ecf60d9 100644 --- a/cmd/pullrequest-init/main.go +++ b/cmd/pullrequest-init/main.go @@ -19,14 +19,18 @@ import ( "context" "flag" "fmt" + "os" + + "github.com/tektoncd/pipeline/pkg/pullrequest" "knative.dev/pkg/logging" ) var ( - prURL = flag.String("url", "", "The url of the pull request to initialize.") - path = flag.String("path", "", "Path of directory under which PR will be copied") - mode = flag.String("mode", "download", "Whether to operate in download or upload mode") + prURL = flag.String("url", "", "The url of the pull request to initialize.") + path = flag.String("path", "", "Path of directory under which PR will be copied") + mode = flag.String("mode", "download", "Whether to operate in download or upload mode") + provider = flag.String("provider", "", "The SCM provider to use. Optional") ) func main() { @@ -35,7 +39,8 @@ func main() { defer logger.Sync() ctx := context.Background() - client, err := NewGitHubHandler(ctx, logger, *prURL) + token := os.Getenv("AUTHTOKEN") + client, err := pullrequest.NewSCMHandler(logger, *prURL, *provider, token) if err != nil { logger.Fatalf("error creating GitHub client: %v", err) } @@ -48,13 +53,13 @@ func main() { fmt.Println(err) logger.Fatal(err) } - if err := ToDisk(pr, *path); err != nil { + if err := pullrequest.ToDisk(pr, *path); err != nil { logger.Fatal(err) } case "upload": logger.Info("RUNNING UPLOAD!") - r, err := FromDisk(*path) + r, err := pullrequest.FromDisk(*path) if err != nil { logger.Fatal(err) } diff --git a/docs/resources.md b/docs/resources.md index 4c9bdef4a3c..688eb093580 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -422,7 +422,7 @@ spec: - name: url value: https://github.com/wizzbangcorp/wizzbang/pulls/1 secrets: - - fieldName: githubToken + - fieldName: authToken secretName: github-secrets secretKey: token --- @@ -439,16 +439,18 @@ data: Params that can be added are the following: 1. `url`: represents the location of the pull request to fetch. +1. `provider`: represents the SCM provider to use. This will be "guesed" based on the url if not set. + Valid values are `github` or `gitlab` today. #### Statuses The following status codes are available to use for the Pull Request resource: https://godoc.org/github.com/jenkins-x/go-scm/scm#State -#### GitHub +#### Pull Request -The `pullRequest` resource will look for GitHub OAuth authentication tokens in -spec secrets with a field name called `githubToken`. +The `pullRequest` resource will look for GitHub or Gitlab OAuth authentication tokens in +spec secrets with a field name called `authToken`. URLs should be of the form: https://github.com/tektoncd/pipeline/pull/1 diff --git a/pkg/apis/pipeline/v1alpha1/pull_request_resource.go b/pkg/apis/pipeline/v1alpha1/pull_request_resource.go index e7afd9c95c7..87cac3d9d10 100644 --- a/pkg/apis/pipeline/v1alpha1/pull_request_resource.go +++ b/pkg/apis/pipeline/v1alpha1/pull_request_resource.go @@ -25,8 +25,8 @@ import ( ) const ( - prSource = "pr-source" - githubTokenEnv = "githubToken" + prSource = "pr-source" + authTokenEnv = "authToken" ) // PullRequestResource is an endpoint from which to get data which is required @@ -35,9 +35,11 @@ type PullRequestResource struct { Name string `json:"name"` Type PipelineResourceType `json:"type"` - // GitHub URL pointing to the pull request. + // URL pointing to the pull request. // Example: https://github.com/owner/repo/pulls/1 URL string `json:"url"` + // SCM provider (github or gitlab today). This will be guessed from URL if not set. + Provider string `json:"provider"` // Secrets holds a struct to indicate a field name and corresponding secret name to populate it. Secrets []SecretParam `json:"secrets"` @@ -58,6 +60,8 @@ func NewPullRequestResource(prImage string, r *PipelineResource) (*PullRequestRe for _, param := range r.Spec.Params { if strings.EqualFold(param.Name, "URL") { prResource.URL = param.Value + } else if strings.EqualFold(param.Name, "Provider") { + prResource.Provider = param.Value } } @@ -82,9 +86,10 @@ func (s *PullRequestResource) GetURL() string { // Replacements is used for template replacement on a PullRequestResource inside of a Taskrun. func (s *PullRequestResource) Replacements() map[string]string { return map[string]string{ - "name": s.Name, - "type": string(s.Type), - "url": s.URL, + "name": s.Name, + "type": string(s.Type), + "url": s.URL, + "provider": s.Provider, } } @@ -104,10 +109,13 @@ func (s *PullRequestResource) GetOutputTaskModifier(ts *TaskSpec, sourcePath str func (s *PullRequestResource) getSteps(mode string, sourcePath string) []Step { args := []string{"-url", s.URL, "-path", sourcePath, "-mode", mode} + if s.Provider != "" { + args = append(args, []string{"-provider", s.Provider}...) + } evs := []corev1.EnvVar{} for _, sec := range s.Secrets { - if strings.EqualFold(sec.FieldName, githubTokenEnv) { + if strings.EqualFold(sec.FieldName, authTokenEnv) { ev := corev1.EnvVar{ Name: strings.ToUpper(sec.FieldName), ValueFrom: &corev1.EnvVarSource{ diff --git a/pkg/apis/pipeline/v1alpha1/pull_request_resource_test.go b/pkg/apis/pipeline/v1alpha1/pull_request_resource_test.go index c3291f81d04..dcc5f80288e 100644 --- a/pkg/apis/pipeline/v1alpha1/pull_request_resource_test.go +++ b/pkg/apis/pipeline/v1alpha1/pull_request_resource_test.go @@ -30,9 +30,9 @@ func TestPullRequest_NewResource(t *testing.T) { url := "https://github.com/tektoncd/pipeline/pulls/1" pr := tb.PipelineResource("foo", "default", tb.PipelineResourceSpec( v1alpha1.PipelineResourceTypePullRequest, - tb.PipelineResourceSpecParam("type", "github"), tb.PipelineResourceSpecParam("url", url), - tb.PipelineResourceSpecSecretParam("githubToken", "test-secret-key", "test-secret-name"), + tb.PipelineResourceSpecParam("provider", "github"), + tb.PipelineResourceSpecSecretParam("authToken", "test-secret-key", "test-secret-name"), )) got, err := v1alpha1.NewPullRequestResource("override-with-pr:latest", pr) if err != nil { @@ -40,11 +40,12 @@ func TestPullRequest_NewResource(t *testing.T) { } want := &v1alpha1.PullRequestResource{ - Name: pr.Name, - Type: v1alpha1.PipelineResourceTypePullRequest, - URL: url, - Secrets: pr.Spec.SecretParams, - PRImage: "override-with-pr:latest", + Name: pr.Name, + Type: v1alpha1.PipelineResourceTypePullRequest, + URL: url, + Provider: "github", + Secrets: pr.Spec.SecretParams, + PRImage: "override-with-pr:latest", } if diff := cmp.Diff(want, got); diff != "" { t.Error(diff) @@ -85,20 +86,21 @@ func containerTestCases(mode string) []testcase { Name: "creds", URL: "https://example.com", Secrets: []v1alpha1.SecretParam{{ - FieldName: "githubToken", + FieldName: "authToken", SecretName: "github-creds", SecretKey: "token", }}, - PRImage: "override-with-pr:latest", + PRImage: "override-with-pr:latest", + Provider: "github", }, out: []v1alpha1.Step{{Container: corev1.Container{ Name: "pr-source-creds-mz4c7", Image: "override-with-pr:latest", WorkingDir: v1alpha1.WorkspaceDir, Command: []string{"/ko-app/pullrequest-init"}, - Args: []string{"-url", "https://example.com", "-path", "/workspace", "-mode", mode}, + Args: []string{"-url", "https://example.com", "-path", "/workspace", "-mode", mode, "-provider", "github"}, Env: []corev1.EnvVar{{ - Name: "GITHUBTOKEN", + Name: "AUTHTOKEN", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ diff --git a/cmd/pullrequest-init/api.go b/pkg/pullrequest/api.go similarity index 65% rename from cmd/pullrequest-init/api.go rename to pkg/pullrequest/api.go index 1a202aa7d63..b543da1fe6d 100644 --- a/cmd/pullrequest-init/api.go +++ b/pkg/pullrequest/api.go @@ -14,13 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package pullrequest import ( "context" + "fmt" "io/ioutil" "strconv" + "golang.org/x/xerrors" + "github.com/hashicorp/go-multierror" "github.com/jenkins-x/go-scm/scm" "go.uber.org/zap" @@ -53,31 +56,37 @@ func (h *Handler) Download(ctx context.Context) (*Resource, error) { h.logger.Info("finding pr") pr, _, err := h.client.PullRequests.Find(ctx, h.repo, h.prNum) if err != nil { - h.logger.Infof("finding pr: %v", err) - return nil, err + return nil, xerrors.Errorf("finding pr: %s", h.prNum, err) } // Statuses h.logger.Info("finding combined status") - status, out, err := h.client.Repositories.FindCombinedStatus(ctx, h.repo, pr.Sha) + status, out, err := h.client.Repositories.ListStatus(ctx, h.repo, pr.Sha, scm.ListOptions{}) if err != nil { body, _ := ioutil.ReadAll(out.Body) defer out.Body.Close() h.logger.Warnf("%v: %s", err, string(body)) - return nil, err + return nil, xerrors.Errorf("finding combined status: %s", h.prNum, err) } // Comments // TODO: Pagination. - h.logger.Info("finding comments") - comments, _, err := h.client.Issues.ListComments(ctx, h.repo, h.prNum, scm.ListOptions{}) + h.logger.Info("finding comments: %v", h) + comments, _, err := h.client.PullRequests.ListComments(ctx, h.repo, h.prNum, scm.ListOptions{}) + if err != nil { + return nil, xerrors.Errorf("finding comments: %s", h.prNum, err) + } + h.logger.Info("found comments: %v", comments) + + labels, _, err := h.client.PullRequests.ListLabels(ctx, h.repo, h.prNum, scm.ListOptions{}) if err != nil { - return nil, err + return nil, xerrors.Errorf("finding labels: %s", h.prNum, err) } + pr.Labels = labels r := &Resource{ PR: pr, - Status: status, + Statuses: status, Comments: comments, } populateManifest(r) @@ -112,7 +121,7 @@ func (h *Handler) Upload(ctx context.Context, r *Resource) error { merr = multierror.Append(merr, err) } - if err := h.uploadStatuses(ctx, r.Status); err != nil { + if err := h.uploadStatuses(ctx, r.Statuses); err != nil { merr = multierror.Append(merr, err) } @@ -133,15 +142,15 @@ func (h *Handler) uploadLabels(ctx context.Context, manifest Manifest, raw []*sc // Fetch current labels associated to the PR. We'll need to keep track of // which labels are new and should not be modified. - currentLabels, _, err := h.client.Issues.ListLabels(ctx, h.repo, h.prNum, scm.ListOptions{}) + currentLabels, _, err := h.client.PullRequests.ListLabels(ctx, h.repo, h.prNum, scm.ListOptions{}) if err != nil { - return err + return xerrors.Errorf("listing labels %s: %w", h.prNum, err) } current := make(map[string]bool) for _, l := range currentLabels { current[l.Name] = true } - h.logger.Infof("Current labels: %v", current) + h.logger.Debugf("Current labels: %v", current) var merr error @@ -152,10 +161,11 @@ func (h *Handler) uploadLabels(ctx context.Context, manifest Manifest, raw []*sc create = append(create, l) } } - h.logger.Infof("Creating labels %v for PR %d", create, h.prNum) + h.logger.Debugf("Creating labels %v for PR %d", create, h.prNum) for _, l := range create { - if _, err := h.client.Issues.AddLabel(ctx, h.repo, h.prNum, l); err != nil { - merr = multierror.Append(merr, err) + if _, err := h.client.PullRequests.AddLabel(ctx, h.repo, h.prNum, l); err != nil { + fmt.Println("ERROR:", err) + merr = multierror.Append(merr, xerrors.Errorf("adding label %s: %w", l, err)) } } @@ -163,14 +173,14 @@ func (h *Handler) uploadLabels(ctx context.Context, manifest Manifest, raw []*sc // the manifest. for l := range current { if !labels[l] && manifest[l] { - h.logger.Infof("Removing label %s for PR %d", l, h.prNum) - if _, err := h.client.Issues.DeleteLabel(ctx, h.repo, h.prNum, l); err != nil { - merr = multierror.Append(merr, err) + h.logger.Debugf("Removing label %s for PR %d", l, h.prNum) + if _, err := h.client.PullRequests.DeleteLabel(ctx, h.repo, h.prNum, l); err != nil { + merr = multierror.Append(merr, xerrors.Errorf("finding pr %s: %w", h.prNum, err)) } } } - return err + return merr } func (h *Handler) uploadComments(ctx context.Context, manifest Manifest, comments []*scm.Comment) error { @@ -190,11 +200,11 @@ func (h *Handler) uploadComments(ctx context.Context, manifest Manifest, comment var merr error if err := h.maybeDeleteComments(ctx, manifest, existingComments); err != nil { - merr = multierror.Append(merr, err) + merr = multierror.Append(merr, xerrors.Errorf("deleting comments: %s", existingComments, err)) } if err := h.createNewComments(ctx, newComments); err != nil { - merr = multierror.Append(merr, err) + merr = multierror.Append(merr, xerrors.Errorf("creating comments: %s", newComments, err)) } return merr @@ -204,7 +214,7 @@ func (h *Handler) uploadComments(ctx context.Context, manifest Manifest, comment // SCM and exists in the manifest (therefore was present during resource // initialization). func (h *Handler) maybeDeleteComments(ctx context.Context, manifest Manifest, comments map[int]*scm.Comment) error { - currentComments, _, err := h.client.Issues.ListComments(ctx, h.repo, h.prNum, scm.ListOptions{}) + currentComments, _, err := h.client.PullRequests.ListComments(ctx, h.repo, h.prNum, scm.ListOptions{}) if err != nil { return err } @@ -223,16 +233,15 @@ func (h *Handler) maybeDeleteComments(ctx context.Context, manifest Manifest, co if _, ok := manifest[strconv.Itoa(ec.ID)]; !ok { // Comment did not exist when resource created, so this was created // recently. To not modify this comment. - h.logger.Infof("Not tracking comment %d. Skipping.", ec.ID) + h.logger.Debugf("Not tracking comment %d. Skipping.", ec.ID) continue } // Comment existed beforehand, user intentionally deleted. Remove from // upstream source. h.logger.Infof("Deleting comment %d for PR %d", ec.ID, h.prNum) - if _, err := h.client.Issues.DeleteComment(ctx, h.repo, h.prNum, ec.ID); err != nil { - h.logger.Warnf("Error deleting comment: %v", err) - merr = multierror.Append(merr, err) + if _, err := h.client.PullRequests.DeleteComment(ctx, h.repo, h.prNum, ec.ID); err != nil { + merr = multierror.Append(merr, xerrors.Errorf("deleting comment: %s", ec.ID, err)) continue } } @@ -246,34 +255,44 @@ func (h *Handler) createNewComments(ctx context.Context, comments []*scm.Comment Body: dc.Body, } h.logger.Infof("Creating comment %s for PR %d", c.Body, h.prNum) - if _, _, err := h.client.Issues.CreateComment(ctx, h.repo, h.prNum, c); err != nil { - h.logger.Warnf("Error creating comment: %v", err) - merr = multierror.Append(merr, err) + if _, _, err := h.client.PullRequests.CreateComment(ctx, h.repo, h.prNum, c); err != nil { + merr = multierror.Append(merr, xerrors.Errorf("creating comment: %s", c, err)) } } return merr } -func (h *Handler) uploadStatuses(ctx context.Context, status *scm.CombinedStatus) error { - if status == nil { +func (h *Handler) uploadStatuses(ctx context.Context, statuses []*scm.Status) error { + if statuses == nil { + h.logger.Info("Skipping statuses, nothing to set.") return nil } - cs, _, err := h.client.Repositories.FindCombinedStatus(ctx, h.repo, status.Sha) + // We have to retrieve the PR to set the status one more time, the commits could have changed. + pr, _, err := h.client.PullRequests.Find(ctx, h.repo, h.prNum) + if err != nil { + return err + } + h.logger.Infof("Looking for existing status on %s", pr.Sha) + cs, _, err := h.client.Repositories.ListStatus(ctx, h.repo, pr.Sha, scm.ListOptions{}) if err != nil { return err } // Index the statuses so we can avoid sending them if they already exist. csMap := map[string]scm.State{} - for _, s := range cs.Statuses { + for _, s := range cs { csMap[s.Label] = s.State } var merr error - for _, s := range status.Statuses { + for _, s := range statuses { + h.logger.Info(s.Label) + h.logger.Info(s.Desc) + h.logger.Info(s.State) if csMap[s.Label] == s.State { + h.logger.Infof("Skipping setting %s because it already matches", s.Label) continue } @@ -283,8 +302,9 @@ func (h *Handler) uploadStatuses(ctx context.Context, status *scm.CombinedStatus Desc: s.Desc, Target: s.Target, } - if _, _, err := h.client.Repositories.CreateStatus(ctx, h.repo, status.Sha, si); err != nil { - merr = multierror.Append(merr, err) + h.logger.Infof("Creating status %s on %s", si.Label, pr.Sha) + if _, _, err := h.client.Repositories.CreateStatus(ctx, h.repo, pr.Sha, si); err != nil { + merr = multierror.Append(merr, xerrors.Errorf("creating status: %s", si.Label, err)) continue } } diff --git a/cmd/pullrequest-init/api_test.go b/pkg/pullrequest/api_test.go similarity index 92% rename from cmd/pullrequest-init/api_test.go rename to pkg/pullrequest/api_test.go index 3e3580bb604..4f44b907bc4 100644 --- a/cmd/pullrequest-init/api_test.go +++ b/pkg/pullrequest/api_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package pullrequest import ( "context" @@ -49,7 +49,7 @@ func defaultResource() *Resource { Sha: "sha2", Repo: scm.Repository{Name: "repo1"}, }, - Labels: []*scm.Label{{Name: "tacocat"}}, + Labels: []*scm.Label{}, } r := &Resource{ PR: pr, @@ -63,14 +63,13 @@ func defaultResource() *Resource { Body: "abc123", }, }, - Status: &scm.CombinedStatus{ - Sha: pr.Sha, - Statuses: []*scm.Status{{ + Statuses: []*scm.Status{ + { Label: "Tekton", State: scm.StateSuccess, Desc: "Test all the things!", Target: "https://tekton.dev", - }}, + }, }, } populateManifest(r) @@ -84,7 +83,8 @@ func newHandler(t *testing.T) (*Handler, *fake.Data) { r := defaultResource() data.PullRequests[prNum] = r.PR data.IssueComments[prNum] = r.Comments - data.Statuses[r.PR.Sha] = r.Status.Statuses + data.PullRequestComments[prNum] = r.Comments + data.Statuses[r.PR.Sha] = r.Statuses return NewHandler(logger, client, repo, prNum), data } @@ -103,14 +103,15 @@ func TestDownload(t *testing.T) { t.Fatal(err) } + labels := []*scm.Label{} + for _, l := range data.PullRequestLabelsExisting { + labels = append(labels, &scm.Label{Name: l}) + } pr := data.PullRequests[prNum] want := &Resource{ PR: pr, Comments: data.IssueComments[prNum], - Status: &scm.CombinedStatus{ - Sha: data.PullRequests[prNum].Sha, - Statuses: data.Statuses[pr.Sha], - }, + Statuses: data.Statuses[pr.Sha], } populateManifest(want) @@ -234,7 +235,7 @@ func TestUpload_NewStatus(t *testing.T) { Label: "CI", State: scm.StateFailure, } - r.Status.Statuses = append(r.Status.Statuses, s) + r.Statuses = append(r.Statuses, s) if err := h.Upload(ctx, r); err != nil { t.Fatal(err) @@ -255,7 +256,7 @@ func TestUpload_UpdateStatus(t *testing.T) { h, _ := newHandler(t) r := defaultResource() - r.Status.Statuses[0].State = scm.StateCanceled + r.Statuses[0].State = scm.StateCanceled if err := h.Upload(ctx, r); err != nil { t.Fatal(err) @@ -321,7 +322,7 @@ func TestUpload_ManifestLabel(t *testing.T) { // Create a new label out of band of the resource. The upload should not // affect this. - if _, err := h.client.Issues.AddLabel(ctx, h.repo, h.prNum, "z"); err != nil { + if _, err := h.client.PullRequests.AddLabel(ctx, h.repo, h.prNum, "z"); err != nil { t.Fatal(err) } diff --git a/cmd/pullrequest-init/disk.go b/pkg/pullrequest/disk.go similarity index 96% rename from cmd/pullrequest-init/disk.go rename to pkg/pullrequest/disk.go index bc6cfab766d..668388360f3 100644 --- a/cmd/pullrequest-init/disk.go +++ b/pkg/pullrequest/disk.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package pullrequest import ( "encoding/gob" @@ -51,7 +51,7 @@ const ( // Resource represents a complete SCM resource that should be recorded to disk. type Resource struct { PR *scm.PullRequest - Status *scm.CombinedStatus + Statuses []*scm.Status Comments []*scm.Comment // Manifests contain data about the resource when it was written to disk. @@ -83,7 +83,7 @@ func ToDisk(r *Resource, path string) error { } // Now status - if err := statusToDisk(statusesPath, r.Status.Statuses); err != nil { + if err := statusToDisk(statusesPath, r.Statuses); err != nil { log.Print(err) return err @@ -183,7 +183,7 @@ func FromDisk(path string) (*Resource, error) { r.Manifests["labels"] = manifest statusesPath := filepath.Join(path, "status") - r.Status, err = statusesFromDisk(statusesPath) + r.Statuses, err = statusesFromDisk(statusesPath) if err != nil { return nil, err } @@ -284,7 +284,7 @@ func labelsFromDisk(path string) ([]*scm.Label, Manifest, error) { return labels, manifest, nil } -func statusesFromDisk(path string) (*scm.CombinedStatus, error) { +func statusesFromDisk(path string) ([]*scm.Status, error) { fis, err := ioutil.ReadDir(path) if err != nil { return nil, err @@ -302,9 +302,7 @@ func statusesFromDisk(path string) (*scm.CombinedStatus, error) { } statuses = append(statuses, &status) } - return &scm.CombinedStatus{ - Statuses: statuses, - }, nil + return statuses, nil } func refFromDisk(path, name string) (scm.PullRequestBranch, error) { diff --git a/cmd/pullrequest-init/disk_test.go b/pkg/pullrequest/disk_test.go similarity index 96% rename from cmd/pullrequest-init/disk_test.go rename to pkg/pullrequest/disk_test.go index 697ba28b202..d61b1646165 100644 --- a/cmd/pullrequest-init/disk_test.go +++ b/pkg/pullrequest/disk_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package pullrequest import ( "encoding/json" @@ -51,22 +51,18 @@ func TestToDisk(t *testing.T) { {Name: "foo/bar"}, }, }, - Status: &scm.CombinedStatus{ - State: scm.StateSuccess, - Sha: "sha1", - Statuses: []*scm.Status{ - { - Label: "123", - State: scm.StateSuccess, - Desc: "foobar", - Target: "https://foo.bar", - }, - { - Label: "cla/foo", - State: scm.StateSuccess, - Desc: "bazbat", - Target: "https://baz.bat", - }, + Statuses: []*scm.Status{ + { + Label: "123", + State: scm.StateSuccess, + Desc: "foobar", + Target: "https://foo.bar", + }, + { + Label: "cla/foo", + State: scm.StateSuccess, + Desc: "bazbat", + Target: "https://baz.bat", }, }, Comments: []*scm.Comment{{ @@ -107,7 +103,7 @@ func TestToDisk(t *testing.T) { readAndUnmarshal(t, filepath.Join(d, "status", fi.Name()), &status) statuses[status.Target] = status } - for _, s := range rsrc.Status.Statuses { + for _, s := range rsrc.Statuses { actualStatus, ok := statuses[s.Target] if !ok { t.Errorf("Expected status with ID: %s, not found: %v", s.Target, statuses) @@ -346,7 +342,7 @@ func TestFromDisk(t *testing.T) { for _, s := range statuses { statusMap[s.Label] = s } - for _, s := range rsrc.Status.Statuses { + for _, s := range rsrc.Statuses { if diff := cmp.Diff(statusMap[s.Label], *s); diff != "" { t.Errorf("Get status: -want +got: %s", diff) } diff --git a/pkg/pullrequest/scm.go b/pkg/pullrequest/scm.go new file mode 100644 index 00000000000..aa0910d5585 --- /dev/null +++ b/pkg/pullrequest/scm.go @@ -0,0 +1,133 @@ +/* +Copyright 2019 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pullrequest + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "golang.org/x/oauth2" + + "github.com/jenkins-x/go-scm/scm/driver/github" + "github.com/jenkins-x/go-scm/scm/driver/gitlab" + "go.uber.org/zap" +) + +func NewSCMHandler(logger *zap.SugaredLogger, raw, provider, token string) (*Handler, error) { + u, err := url.Parse(raw) + if err != nil { + return nil, err + } + + if provider == "" { + p, err := guessProvider(raw) + if err != nil { + return nil, err + } + provider = p + } + + var handler *Handler + switch provider { + case "github": + handler, err = githubHandlerFromURL(u, token, logger) + case "gitlab": + handler, err = gitlabHandlerFromURL(u, token, logger) + default: + return nil, fmt.Errorf("unsupported pr url: %s", raw) + } + return handler, nil +} + +func githubHandlerFromURL(u *url.URL, token string, logger *zap.SugaredLogger) (*Handler, error) { + split := strings.Split(u.Path, "/") + if len(split) < 5 { + return nil, fmt.Errorf("could not determine PR from URL: %v", u) + } + owner, repo, pr := split[1], split[2], split[4] + prNumber, err := strconv.Atoi(pr) + if err != nil { + return nil, fmt.Errorf("error parsing PR number: %s", pr) + } + + client := github.NewDefault() + ownerRepo := fmt.Sprintf("%s/%s", owner, repo) + h := NewHandler(logger, client, ownerRepo, prNumber) + if token != "" { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + h.client.Client = oauth2.NewClient(context.Background(), ts) + } + return h, nil +} + +func gitlabHandlerFromURL(u *url.URL, token string, logger *zap.SugaredLogger) (*Handler, error) { + // The project name can be multiple /'s deep, so split on / and work from right to left. + split := strings.Split(u.Path, "/") + + // The PR number should be the last element. + last := len(split) - 1 + prNum := split[last] + prInt, err := strconv.Atoi(prNum) + if err != nil { + return nil, fmt.Errorf("unable to parse pr as number from %s", u) + } + + // Next we sanity check that this is a correct url. The next to last element should be "merge_requests" + if split[last-1] != "merge_requests" { + return nil, fmt.Errorf("invalid gitlab url: %s", u) + } + + // Next, we rejoin everything else into the project field. + project := strings.Join(split[1:last-1], "/") + client := gitlab.NewDefault() + if token != "" { + client.Client = &http.Client{ + Transport: &gitlabClient{ + token: token, + transport: http.DefaultTransport, + }, + } + } + return NewHandler(logger, client, project, prInt), nil +} + +// gitlab client wraps a normal http client, adding support for private-token auth. +type gitlabClient struct { + token string + transport http.RoundTripper +} + +func (g *gitlabClient) RoundTrip(r *http.Request) (*http.Response, error) { + r.Header.Add("Private-Token", g.token) + return g.transport.RoundTrip(r) +} + +func guessProvider(url string) (string, error) { + switch { + case strings.Contains(url, "github"): + return "github", nil + case strings.Contains(url, "gitlab"): + return "gitlab", nil + } + return "", fmt.Errorf("unable to guess scm provider from url: %s", url) +} diff --git a/pkg/pullrequest/scm_test.go b/pkg/pullrequest/scm_test.go new file mode 100644 index 00000000000..bd94ff78921 --- /dev/null +++ b/pkg/pullrequest/scm_test.go @@ -0,0 +1,133 @@ +/* +Copyright 2019 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pullrequest + +import ( + "reflect" + "testing" + + "go.uber.org/zap" +) + +func TestNewSCMHandler(t *testing.T) { + tests := []struct { + name string + raw string + wantRepo string + wantNum int + wantErr bool + }{ + { + name: "github", + raw: "https://github.com/foo/bar/pull/1", + wantRepo: "foo/bar", + wantNum: 1, + wantErr: false, + }, + { + name: "custom github", + raw: "https://github.tekton.dev/foo/baz/pull/2", + wantRepo: "foo/baz", + wantNum: 2, + wantErr: false, + }, + { + name: "gitlab", + raw: "https://gitlab.com/foo/bar/merge_requests/3", + wantRepo: "foo/bar", + wantNum: 3, + wantErr: false, + }, + { + name: "gitlab multi-level", + raw: "https://gitlab.com/foo/bar/baz/merge_requests/3", + wantRepo: "foo/bar/baz", + wantNum: 3, + wantErr: false, + }, + { + name: "unsupported", + raw: "https://unsupported.com/foo/baz/merge_requests/3", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := &zap.SugaredLogger{} + got, err := NewSCMHandler(logger, tt.raw, "", "") + if err != nil { + if !tt.wantErr { + t.Errorf("NewSCMHandler() error = %v, wantErr %v", err, tt.wantErr) + } + return + } + if !reflect.DeepEqual(got.prNum, tt.wantNum) { + t.Errorf("NewSCMHandler() = %v, want %v", got, tt.wantNum) + } + if !reflect.DeepEqual(got.repo, tt.wantRepo) { + t.Errorf("NewSCMHandler() = %v, want %v", got, tt.wantRepo) + } + }) + } +} + +func Test_guessProvider(t *testing.T) { + tests := []struct { + name string + url string + want string + wantErr bool + }{ + { + name: "github", + url: "https://github.com/foo/bar", + want: "github", + }, + { + name: "nested github", + url: "https://github.foo.com/foo/bar", + want: "github", + }, + { + name: "gitlab", + url: "https://gitlab.com/foo/bar", + want: "gitlab", + }, + { + name: "nested gitlab", + url: "https://gitlab.foo.com/foo/bar", + want: "gitlab", + }, + { + name: "err", + url: "https://foo.com/foo/bar", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := guessProvider(tt.url) + if (err != nil) != tt.wantErr { + t.Errorf("guessProvider() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("guessProvider() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/const.go b/vendor/github.com/jenkins-x/go-scm/scm/const.go index 9194de88766..6e8c55eb51b 100644 --- a/vendor/github.com/jenkins-x/go-scm/scm/const.go +++ b/vendor/github.com/jenkins-x/go-scm/scm/const.go @@ -107,6 +107,9 @@ const ( ActionEdited ActionSubmitted ActionDismissed + + // check run / check suite + ActionCompleted ) // String returns the string representation of Action. @@ -148,6 +151,8 @@ func (a Action) String() (s string) { return "review_request_removed" case ActionReadyForReview: return "ready_for_review" + case ActionCompleted: + return "completed" default: return } @@ -185,6 +190,8 @@ func (a *Action) UnmarshalJSON(data []byte) error { *a = ActionSync case "merged": *a = ActionMerge + case "completed": + *a = ActionCompleted } return nil } diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/fake/data.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/fake/data.go index bda30713661..eba055685b2 100644 --- a/vendor/github.com/jenkins-x/go-scm/scm/driver/fake/data.go +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/fake/data.go @@ -3,20 +3,23 @@ package fake import "github.com/jenkins-x/go-scm/scm" type Data struct { - Issues map[int][]*scm.Issue - OrgMembers map[string][]string - Collaborators []string - IssueComments map[int][]*scm.Comment - IssueCommentID int - PullRequests map[int]*scm.PullRequest - PullRequestChanges map[int][]*scm.Change - PullRequestComments map[int][]*scm.Comment - ReviewID int - Reviews map[int][]*scm.Review - Statuses map[string][]*scm.Status - IssueEvents map[int][]*scm.ListedIssueEvent - Commits map[string]*scm.Commit - TestRef string + Issues map[int][]*scm.Issue + OrgMembers map[string][]string + Collaborators []string + IssueComments map[int][]*scm.Comment + IssueCommentID int + PullRequests map[int]*scm.PullRequest + PullRequestChanges map[int][]*scm.Change + PullRequestComments map[int][]*scm.Comment + PullRequestLabelsAdded []string + PullRequestLabelsRemoved []string + PullRequestLabelsExisting []string + ReviewID int + Reviews map[int][]*scm.Review + Statuses map[string][]*scm.Status + IssueEvents map[int][]*scm.ListedIssueEvent + Commits map[string]*scm.Commit + TestRef string //All Labels That Exist In The Repo RepoLabelsExisting []string @@ -62,29 +65,32 @@ type DeletedRef struct { // NewData create a new set of fake data func NewData() *Data { return &Data{ - Issues: map[int][]*scm.Issue{}, - OrgMembers: map[string][]string{}, - Collaborators: []string{}, - IssueComments: map[int][]*scm.Comment{}, - PullRequests: map[int]*scm.PullRequest{}, - PullRequestChanges: map[int][]*scm.Change{}, - PullRequestComments: map[int][]*scm.Comment{}, - Reviews: map[int][]*scm.Review{}, - Statuses: map[string][]*scm.Status{}, - IssueEvents: map[int][]*scm.ListedIssueEvent{}, - Commits: map[string]*scm.Commit{}, - MilestoneMap: map[string]int{}, - CommitMap: map[string][]scm.Commit{}, - RemoteFiles: map[string]map[string]string{}, - TestRef: "abcde", - IssueLabelsAdded: []string{}, - IssueLabelsExisting: []string{}, - IssueLabelsRemoved: []string{}, - IssueCommentsAdded: []string{}, - IssueCommentsDeleted: []string{}, - IssueReactionsAdded: []string{}, - CommentReactionsAdded: []string{}, - AssigneesAdded: []string{}, - UserPermissions: map[string]map[string]string{}, + Issues: map[int][]*scm.Issue{}, + OrgMembers: map[string][]string{}, + Collaborators: []string{}, + IssueComments: map[int][]*scm.Comment{}, + PullRequests: map[int]*scm.PullRequest{}, + PullRequestChanges: map[int][]*scm.Change{}, + PullRequestComments: map[int][]*scm.Comment{}, + PullRequestLabelsAdded: []string{}, + PullRequestLabelsRemoved: []string{}, + PullRequestLabelsExisting: []string{}, + Reviews: map[int][]*scm.Review{}, + Statuses: map[string][]*scm.Status{}, + IssueEvents: map[int][]*scm.ListedIssueEvent{}, + Commits: map[string]*scm.Commit{}, + MilestoneMap: map[string]int{}, + CommitMap: map[string][]scm.Commit{}, + RemoteFiles: map[string]map[string]string{}, + TestRef: "abcde", + IssueLabelsAdded: []string{}, + IssueLabelsExisting: []string{}, + IssueLabelsRemoved: []string{}, + IssueCommentsAdded: []string{}, + IssueCommentsDeleted: []string{}, + IssueReactionsAdded: []string{}, + CommentReactionsAdded: []string{}, + AssigneesAdded: []string{}, + UserPermissions: map[string]map[string]string{}, } } diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/fake/pr.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/fake/pr.go index 8440e7b4aeb..2f8b8a94f68 100644 --- a/vendor/github.com/jenkins-x/go-scm/scm/driver/fake/pr.go +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/fake/pr.go @@ -42,6 +42,34 @@ func (s *pullService) ListComments(ctx context.Context, repo string, number int, return append([]*scm.Comment{}, f.PullRequestComments[number]...), nil, nil } +func (s *pullService) ListLabels(context.Context, string, int, scm.ListOptions) ([]*scm.Label, *scm.Response, error) { + r := []*scm.Label{} + for _, l := range s.data.PullRequestLabelsExisting { + r = append(r, &scm.Label{ + Name: l, + }) + } + return r, nil, nil +} + +func (s *pullService) AddLabel(ctx context.Context, repo string, number int, label string) (*scm.Response, error) { + s.data.PullRequestLabelsExisting = append(s.data.PullRequestLabelsExisting, label) + return nil, nil +} + +func (s *pullService) DeleteLabel(ctx context.Context, repo string, number int, label string) (*scm.Response, error) { + s.data.PullRequestLabelsRemoved = append(s.data.PullRequestLabelsRemoved, label) + + left := []string{} + for _, l := range s.data.PullRequestLabelsExisting { + if l != label { + left = append(left, l) + } + } + s.data.PullRequestLabelsExisting = left + return nil, nil +} + func (s *pullService) Merge(context.Context, string, int) (*scm.Response, error) { panic("implement me") } @@ -52,17 +80,24 @@ func (s *pullService) Close(context.Context, string, int) (*scm.Response, error) func (s *pullService) CreateComment(ctx context.Context, repo string, number int, comment *scm.CommentInput) (*scm.Comment, *scm.Response, error) { f := s.data - f.IssueCommentsAdded = append(f.IssueCommentsAdded, fmt.Sprintf("%s#%d:%s", repo, number, comment.Body)) answer := &scm.Comment{ ID: f.IssueCommentID, Body: comment.Body, Author: scm.User{Login: botName}, } - f.IssueComments[number] = append(f.IssueComments[number], answer) + f.PullRequestComments[number] = append(f.PullRequestComments[number], answer) f.IssueCommentID++ return answer, nil, nil } -func (s *pullService) DeleteComment(context.Context, string, int, int) (*scm.Response, error) { - panic("implement me") +func (s *pullService) DeleteComment(ctx context.Context, repo string, number int, id int) (*scm.Response, error) { + f := s.data + newComments := []*scm.Comment{} + for _, c := range f.PullRequestComments[number] { + if c.ID != id { + newComments = append(newComments, c) + } + } + f.PullRequestComments[number] = newComments + return nil, nil } diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/github/webhook.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/github/webhook.go index 55fba43b026..a643a541a79 100644 --- a/vendor/github.com/jenkins-x/go-scm/scm/driver/github/webhook.go +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/github/webhook.go @@ -51,25 +51,37 @@ func (s *webhookService) Parse(req *http.Request, fn scm.SecretFunc) (scm.Webhoo var hook scm.Webhook event := req.Header.Get("X-GitHub-Event") switch event { - case "ping": - hook, err = s.parsePingHook(data, guid) - case "push": - hook, err = s.parsePushHook(data, guid) + case "check_run": + hook, err = s.parseCheckRunHook(data) + case "check_suite": + hook, err = s.parseCheckSuiteHook(data) case "create": hook, err = s.parseCreateHook(data) case "delete": hook, err = s.parseDeleteHook(data) - case "pull_request": - hook, err = s.parsePullRequestHook(data, guid) - case "pull_request_review_comment": - hook, err = s.parsePullRequestReviewCommentHook(data) case "deployment": hook, err = s.parseDeploymentHook(data) + case "deployment_status": + hook, err = s.parseDeploymentStatusHook(data) // case "issues": case "issue_comment": hook, err = s.parseIssueCommentHook(data) case "installation", "integration_installation": hook, err = s.parseInstallationHook(data) + case "label": + hook, err = s.parseLabelHook(data) + case "ping": + hook, err = s.parsePingHook(data, guid) + case "push": + hook, err = s.parsePushHook(data, guid) + case "pull_request": + hook, err = s.parsePullRequestHook(data, guid) + case "pull_request_review_comment": + hook, err = s.parsePullRequestReviewCommentHook(data) + case "release": + hook, err = s.parseReleaseHook(data) + case "status": + hook, err = s.parseStatusHook(data) default: log.WithField("Event", event).Warnf("unknown webhook") return nil, scm.UnknownWebhook{event} @@ -148,6 +160,66 @@ func (s *webhookService) parseDeleteHook(data []byte) (scm.Webhook, error) { return dst, nil } +func (s *webhookService) parseCheckRunHook(data []byte) (scm.Webhook, error) { + src := new(checkRunHook) + err := json.Unmarshal(data, src) + if err != nil { + return nil, err + } + to := convertCheckRunHook(src) + return to, err +} + +func (s *webhookService) parseCheckSuiteHook(data []byte) (scm.Webhook, error) { + src := new(checkSuiteHook) + err := json.Unmarshal(data, src) + if err != nil { + return nil, err + } + to := convertCheckSuiteHook(src) + return to, err +} + +func (s *webhookService) parseDeploymentStatusHook(data []byte) (scm.Webhook, error) { + src := new(deploymentStatusHook) + err := json.Unmarshal(data, src) + if err != nil { + return nil, err + } + to := convertDeploymentStatusHook(src) + return to, err +} + +func (s *webhookService) parseLabelHook(data []byte) (scm.Webhook, error) { + src := new(labelHook) + err := json.Unmarshal(data, src) + if err != nil { + return nil, err + } + to := convertLabelHook(src) + return to, err +} + +func (s *webhookService) parseReleaseHook(data []byte) (scm.Webhook, error) { + src := new(releaseHook) + err := json.Unmarshal(data, src) + if err != nil { + return nil, err + } + to := convertReleaseHook(src) + return to, err +} + +func (s *webhookService) parseStatusHook(data []byte) (scm.Webhook, error) { + src := new(statusHook) + err := json.Unmarshal(data, src) + if err != nil { + return nil, err + } + to := convertStatusHook(src) + return to, err +} + func (s *webhookService) parseDeploymentHook(data []byte) (scm.Webhook, error) { src := new(deploymentHook) err := json.Unmarshal(data, src) @@ -246,6 +318,59 @@ type ( Installation *installationRef `json:"installation"` } + // github check_run payload + checkRunHook struct { + Action string `json:"action"` + Repository repository `json:"repository"` + Sender user `json:"sender"` + Label label `json:"label"` + Installation *installationRef `json:"installation"` + } + + // github check_suite payload + checkSuiteHook struct { + Action string `json:"action"` + Repository repository `json:"repository"` + Sender user `json:"sender"` + Label label `json:"label"` + Installation *installationRef `json:"installation"` + } + + // github deployment_status payload + deploymentStatusHook struct { + Action string `json:"action"` + Repository repository `json:"repository"` + Sender user `json:"sender"` + Label label `json:"label"` + Installation *installationRef `json:"installation"` + } + + // github label payload + labelHook struct { + Action string `json:"action"` + Repository repository `json:"repository"` + Sender user `json:"sender"` + Label label `json:"label"` + Installation *installationRef `json:"installation"` + } + + // github release payload + releaseHook struct { + Action string `json:"action"` + Repository repository `json:"repository"` + Sender user `json:"sender"` + Label label `json:"label"` + Installation *installationRef `json:"installation"` + } + + // github status payload + statusHook struct { + Repository repository `json:"repository"` + Sender user `json:"sender"` + Label label `json:"label"` + Installation *installationRef `json:"installation"` + } + pushCommit struct { ID string `json:"id"` TreeID string `json:"tree_id"` @@ -514,6 +639,65 @@ func convertPingHook(dst *pingHook) *scm.PingHook { } } +func convertCheckRunHook(dst *checkRunHook) *scm.CheckRunHook { + return &scm.CheckRunHook{ + Action: convertAction(dst.Action), + Repo: *convertRepository(&dst.Repository), + Sender: *convertUser(&dst.Sender), + Label: convertLabel(dst.Label), + Installation: convertInstallationRef(dst.Installation), + } +} + +func convertCheckSuiteHook(dst *checkSuiteHook) *scm.CheckSuiteHook { + return &scm.CheckSuiteHook{ + Action: convertAction(dst.Action), + Repo: *convertRepository(&dst.Repository), + Sender: *convertUser(&dst.Sender), + Label: convertLabel(dst.Label), + Installation: convertInstallationRef(dst.Installation), + } +} + +func convertDeploymentStatusHook(dst *deploymentStatusHook) *scm.DeploymentStatusHook { + return &scm.DeploymentStatusHook{ + Action: convertAction(dst.Action), + Repo: *convertRepository(&dst.Repository), + Sender: *convertUser(&dst.Sender), + Label: convertLabel(dst.Label), + Installation: convertInstallationRef(dst.Installation), + } +} + +func convertLabelHook(dst *labelHook) *scm.LabelHook { + return &scm.LabelHook{ + Action: convertAction(dst.Action), + Repo: *convertRepository(&dst.Repository), + Sender: *convertUser(&dst.Sender), + Label: convertLabel(dst.Label), + Installation: convertInstallationRef(dst.Installation), + } +} + +func convertReleaseHook(dst *releaseHook) *scm.ReleaseHook { + return &scm.ReleaseHook{ + Action: convertAction(dst.Action), + Repo: *convertRepository(&dst.Repository), + Sender: *convertUser(&dst.Sender), + Label: convertLabel(dst.Label), + Installation: convertInstallationRef(dst.Installation), + } +} + +func convertStatusHook(dst *statusHook) *scm.StatusHook { + return &scm.StatusHook{ + Repo: *convertRepository(&dst.Repository), + Sender: *convertUser(&dst.Sender), + Label: convertLabel(dst.Label), + Installation: convertInstallationRef(dst.Installation), + } +} + func convertPushHook(src *pushHook) *scm.PushHook { dst := &scm.PushHook{ Ref: src.Ref, @@ -774,6 +958,8 @@ func convertAction(src string) (action scm.Action) { return scm.ActionMerge case "synchronize", "synchronized": return scm.ActionSync + case "complete", "completed": + return scm.ActionCompleted default: return } diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/content.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/content.go new file mode 100644 index 00000000000..047d7435217 --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/content.go @@ -0,0 +1,64 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gitlab + +import ( + "context" + "encoding/base64" + "fmt" + "net/url" + "strings" + + "github.com/jenkins-x/go-scm/scm" +) + +type contentService struct { + client *wrapper +} + +func (s *contentService) Find(ctx context.Context, repo, path, ref string) (*scm.Content, *scm.Response, error) { + path = url.QueryEscape(path) + path = strings.Replace(path, ".", "%2E", -1) + endpoint := fmt.Sprintf("api/v4/projects/%s/repository/files/%s?ref=%s", encode(repo), path, ref) + out := new(content) + res, err := s.client.do(ctx, "GET", endpoint, nil, out) + raw, berr := base64.StdEncoding.DecodeString(out.Content) + if berr != nil { + // samples in the gitlab documentation use RawStdEncoding + // so we fallback if StdEncoding returns an error. + raw, berr = base64.RawStdEncoding.DecodeString(out.Content) + if berr != nil { + return nil, res, err + } + } + return &scm.Content{ + Path: out.FilePath, + Data: raw, + }, res, err +} + +func (s *contentService) Create(ctx context.Context, repo, path string, params *scm.ContentParams) (*scm.Response, error) { + return nil, scm.ErrNotSupported +} + +func (s *contentService) Update(ctx context.Context, repo, path string, params *scm.ContentParams) (*scm.Response, error) { + return nil, scm.ErrNotSupported +} + +func (s *contentService) Delete(ctx context.Context, repo, path, ref string) (*scm.Response, error) { + return nil, scm.ErrNotSupported +} + +type content struct { + FileName string `json:"file_name"` + FilePath string `json:"file_path"` + Size int `json:"size"` + Encoding string `json:"encoding"` + Content string `json:"content"` + Ref string `json:"ref"` + BlobID string `json:"blob_id"` + CommitID string `json:"commit_id"` + LastCommitID string `json:"last_commit_id"` +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/git.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/git.go new file mode 100644 index 00000000000..a691de2066e --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/git.go @@ -0,0 +1,173 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gitlab + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/jenkins-x/go-scm/scm" +) + +type gitService struct { + client *wrapper +} + +func (s *gitService) FindRef(ctx context.Context, repo, ref string) (string, *scm.Response, error) { + ref = strings.TrimPrefix(ref, "heads/") + path := fmt.Sprintf("api/v4/projects/%s/repository/commits?ref_name=%s", encode(repo), ref) + out := []*commit{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + + if err != nil { + return "", res, err + } + if len(out) > 0 { + commit := out[0] + if commit.ID != "" { + return commit.ID, res, err + } + } + idx := strings.LastIndex(ref, "/") + if idx >= 0 { + ref = ref[idx+1:] + } + return ref, res, err + +} + +func (s *gitService) DeleteRef(ctx context.Context, repo, ref string) (*scm.Response, error) { + panic("implement me") +} + +func (s *gitService) FindBranch(ctx context.Context, repo, name string) (*scm.Reference, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/repository/branches/%s", encode(repo), name) + out := new(branch) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertBranch(out), res, err +} + +func (s *gitService) FindCommit(ctx context.Context, repo, ref string) (*scm.Commit, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/repository/commits/%s", encode(repo), ref) + out := new(commit) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertCommit(out), res, err +} + +func (s *gitService) FindTag(ctx context.Context, repo, name string) (*scm.Reference, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/repository/tags/%s", encode(repo), name) + out := new(branch) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertTag(out), res, err +} + +func (s *gitService) ListBranches(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Reference, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/repository/branches?%s", encode(repo), encodeListOptions(opts)) + out := []*branch{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertBranchList(out), res, err +} + +func (s *gitService) ListCommits(ctx context.Context, repo string, opts scm.CommitListOptions) ([]*scm.Commit, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/repository/commits?%s", encode(repo), encodeCommitListOptions(opts)) + out := []*commit{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertCommitList(out), res, err +} + +func (s *gitService) ListTags(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Reference, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/repository/tags?%s", encode(repo), encodeListOptions(opts)) + out := []*branch{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertTagList(out), res, err +} + +func (s *gitService) ListChanges(ctx context.Context, repo, ref string, opts scm.ListOptions) ([]*scm.Change, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/repository/commits/%s/diff", encode(repo), ref) + out := []*change{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertChangeList(out), res, err +} + +type branch struct { + Name string `json:"name"` + Commit struct { + ID string `json:"id"` + } +} + +type commit struct { + ID string `json:"id"` + Title string `json:"title"` + Message string `json:"message"` + AuthorName string `json:"author_name"` + AuthorEmail string `json:"author_email"` + AuthorDate time.Time `json:"authored_date"` + CommittedDate time.Time `json:"committed_date"` + CommitterName string `json:"committer_name"` + CommitterEmail string `json:"committer_email"` + Created time.Time `json:"created_at"` +} + +func convertCommitList(from []*commit) []*scm.Commit { + to := []*scm.Commit{} + for _, v := range from { + to = append(to, convertCommit(v)) + } + return to +} + +func convertCommit(from *commit) *scm.Commit { + return &scm.Commit{ + Message: from.Message, + Sha: from.ID, + Author: scm.Signature{ + Login: from.AuthorName, + Name: from.AuthorName, + Email: from.AuthorEmail, + Date: from.AuthorDate, + }, + Committer: scm.Signature{ + Login: from.CommitterName, + Name: from.CommitterName, + Email: from.CommitterEmail, + Date: from.CommittedDate, + }, + } +} + +func convertBranchList(from []*branch) []*scm.Reference { + to := []*scm.Reference{} + for _, v := range from { + to = append(to, convertBranch(v)) + } + return to +} + +func convertBranch(from *branch) *scm.Reference { + return &scm.Reference{ + Name: scm.TrimRef(from.Name), + Path: scm.ExpandRef(from.Name, "refs/heads/"), + Sha: from.Commit.ID, + } +} + +func convertTagList(from []*branch) []*scm.Reference { + to := []*scm.Reference{} + for _, v := range from { + to = append(to, convertTag(v)) + } + return to +} + +func convertTag(from *branch) *scm.Reference { + return &scm.Reference{ + Name: scm.TrimRef(from.Name), + Path: scm.ExpandRef(from.Name, "refs/tags/"), + Sha: from.Commit.ID, + } +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/gitlab.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/gitlab.go new file mode 100644 index 00000000000..cf69b1ef2bf --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/gitlab.go @@ -0,0 +1,112 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package gitlab implements a GitLab client. +package gitlab + +import ( + "context" + "encoding/json" + "net/url" + "strconv" + "strings" + + "github.com/jenkins-x/go-scm/scm" +) + +// New returns a new GitLab API client. +func New(uri string) (*scm.Client, error) { + base, err := url.Parse(uri) + if err != nil { + return nil, err + } + if !strings.HasSuffix(base.Path, "/") { + base.Path = base.Path + "/" + } + client := &wrapper{new(scm.Client)} + client.BaseURL = base + // initialize services + client.Driver = scm.DriverGitlab + client.Contents = &contentService{client} + client.Git = &gitService{client} + client.Issues = &issueService{client} + client.Organizations = &organizationService{client} + client.PullRequests = &pullService{client} + client.Repositories = &repositoryService{client} + client.Reviews = &reviewService{client} + client.Users = &userService{client} + client.Webhooks = &webhookService{client} + return client.Client, nil +} + +// NewDefault returns a new GitLab API client using the +// default gitlab.com address. +func NewDefault() *scm.Client { + client, _ := New("https://gitlab.com") + return client +} + +// wraper wraps the Client to provide high level helper functions +// for making http requests and unmarshaling the response. +type wrapper struct { + *scm.Client +} + +// do wraps the Client.Do function by creating the Request and +// unmarshalling the response. +func (c *wrapper) do(ctx context.Context, method, path string, in, out interface{}) (*scm.Response, error) { + req := &scm.Request{ + Method: method, + Path: path, + } + + // execute the http request + res, err := c.Client.Do(ctx, req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + // parse the gitlab request id. + res.ID = res.Header.Get("X-Request-Id") + + // parse the gitlab rate limit details. + res.Rate.Limit, _ = strconv.Atoi( + res.Header.Get("RateLimit-Limit"), + ) + res.Rate.Remaining, _ = strconv.Atoi( + res.Header.Get("RateLimit-Remaining"), + ) + res.Rate.Reset, _ = strconv.ParseInt( + res.Header.Get("RateLimit-Reset"), 10, 64, + ) + + // snapshot the request rate limit + c.Client.SetRate(res.Rate) + + // if an error is encountered, unmarshal and return the + // error response. + if res.Status > 300 { + err := new(Error) + json.NewDecoder(res.Body).Decode(err) + return res, err + } + + if out == nil { + return res, nil + } + + // if a json response is expected, parse and return + // the json response. + return res, json.NewDecoder(res.Body).Decode(out) +} + +// Error represents a GitLab error. +type Error struct { + Message string `json:"message"` +} + +func (e *Error) Error() string { + return e.Message +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/issue.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/issue.go new file mode 100644 index 00000000000..355b4b8fa74 --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/issue.go @@ -0,0 +1,213 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gitlab + +import ( + "context" + "fmt" + "net/url" + "time" + + "github.com/jenkins-x/go-scm/scm" + "github.com/jenkins-x/go-scm/scm/driver/internal/null" +) + +type issueService struct { + client *wrapper +} + +func (s *issueService) Search(context.Context, scm.SearchOptions) ([]*scm.SearchIssue, *scm.Response, error) { + // TODO implemment + return nil, nil, nil +} + +func (s *issueService) AssignIssue(ctx context.Context, repo string, number int, logins []string) (*scm.Response, error) { + panic("implement me") +} + +func (s *issueService) UnassignIssue(ctx context.Context, repo string, number int, logins []string) (*scm.Response, error) { + panic("implement me") +} + +func (s *issueService) ListEvents(context.Context, string, int, scm.ListOptions) ([]*scm.ListedIssueEvent, *scm.Response, error) { + panic("implement me") +} + +func (s *issueService) ListLabels(ctx context.Context, repo string, number int, opts scm.ListOptions) ([]*scm.Label, *scm.Response, error) { + path := fmt.Sprintf("projects/%s/labels?%s", encode(repo), encodeListOptions(opts)) + out := []*label{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertLabelObjects(out), res, err +} + +func (s *issueService) AddLabel(ctx context.Context, repo string, number int, label string) (*scm.Response, error) { + return nil, scm.ErrNotSupported +} + +func (s *issueService) DeleteLabel(ctx context.Context, repo string, number int, label string) (*scm.Response, error) { + return nil, scm.ErrNotSupported +} + +func (s *issueService) Find(ctx context.Context, repo string, number int) (*scm.Issue, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/issues/%d", encode(repo), number) + out := new(issue) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertIssue(out), res, err +} + +func (s *issueService) FindComment(ctx context.Context, repo string, index, id int) (*scm.Comment, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/issues/%d/notes/%d", encode(repo), index, id) + out := new(issueComment) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertIssueComment(out), res, err +} + +func (s *issueService) List(ctx context.Context, repo string, opts scm.IssueListOptions) ([]*scm.Issue, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/issues?%s", encode(repo), encodeIssueListOptions(opts)) + out := []*issue{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertIssueList(out), res, err +} + +func (s *issueService) ListComments(ctx context.Context, repo string, index int, opts scm.ListOptions) ([]*scm.Comment, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/issues/%d/notes?%s", encode(repo), index, encodeListOptions(opts)) + out := []*issueComment{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertIssueCommentList(out), res, err +} + +func (s *issueService) Create(ctx context.Context, repo string, input *scm.IssueInput) (*scm.Issue, *scm.Response, error) { + in := url.Values{} + in.Set("title", input.Title) + in.Set("description", input.Body) + path := fmt.Sprintf("api/v4/projects/%s/issues?%s", encode(repo), in.Encode()) + out := new(issue) + res, err := s.client.do(ctx, "POST", path, nil, out) + return convertIssue(out), res, err +} + +func (s *issueService) CreateComment(ctx context.Context, repo string, number int, input *scm.CommentInput) (*scm.Comment, *scm.Response, error) { + in := url.Values{} + in.Set("body", input.Body) + path := fmt.Sprintf("api/v4/projects/%s/issues/%d/notes?%s", encode(repo), number, in.Encode()) + out := new(issueComment) + res, err := s.client.do(ctx, "POST", path, nil, out) + return convertIssueComment(out), res, err +} + +func (s *issueService) DeleteComment(ctx context.Context, repo string, number, id int) (*scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/issues/%d/notes/%d", encode(repo), number, id) + return s.client.do(ctx, "DELETE", path, nil, nil) +} + +func (s *issueService) Close(ctx context.Context, repo string, number int) (*scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/issues/%d?state_event=close", encode(repo), number) + res, err := s.client.do(ctx, "PUT", path, nil, nil) + return res, err +} + +func (s *issueService) Lock(ctx context.Context, repo string, number int) (*scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/issues/%d?discussion_locked=true", encode(repo), number) + res, err := s.client.do(ctx, "PUT", path, nil, nil) + return res, err +} + +func (s *issueService) Unlock(ctx context.Context, repo string, number int) (*scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/issues/%d?discussion_locked=false", encode(repo), number) + res, err := s.client.do(ctx, "PUT", path, nil, nil) + return res, err +} + +type issue struct { + ID int `json:"id"` + Number int `json:"iid"` + State string `json:"state"` + Title string `json:"title"` + Desc string `json:"description"` + Link string `json:"web_url"` + Locked bool `json:"discussion_locked"` + Labels []string `json:"labels"` + Author struct { + Name string `json:"name"` + Username string `json:"username"` + Avatar null.String `json:"avatar_url"` + } `json:"author"` + Created time.Time `json:"created_at"` + Updated time.Time `json:"updated_at"` +} + +type issueComment struct { + ID int `json:"id"` + Number int `json:"noteable_iid"` + User struct { + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + Name string `json:"name"` + } `json:"author"` + Body string `json:"body"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type issueCommentInput struct { + Body string `json:"body"` +} + +// helper function to convert from the gogs issue list to +// the common issue structure. +func convertIssueList(from []*issue) []*scm.Issue { + to := []*scm.Issue{} + for _, v := range from { + to = append(to, convertIssue(v)) + } + return to +} + +// helper function to convert from the gogs issue structure to +// the common issue structure. +func convertIssue(from *issue) *scm.Issue { + return &scm.Issue{ + Number: from.Number, + Title: from.Title, + Body: from.Desc, + Link: from.Link, + Labels: from.Labels, + Locked: from.Locked, + Closed: from.State == "closed", + Author: scm.User{ + Name: from.Author.Name, + Login: from.Author.Username, + Avatar: from.Author.Avatar.String, + }, + Created: from.Created, + Updated: from.Updated, + } +} + +// helper function to convert from the gogs issue comment list +// to the common issue structure. +func convertIssueCommentList(from []*issueComment) []*scm.Comment { + to := []*scm.Comment{} + for _, v := range from { + to = append(to, convertIssueComment(v)) + } + return to +} + +// helper function to convert from the gogs issue comment to +// the common issue comment structure. +func convertIssueComment(from *issueComment) *scm.Comment { + return &scm.Comment{ + ID: from.ID, + Body: from.Body, + Author: scm.User{ + Name: from.User.Name, + Login: from.User.Username, + Avatar: from.User.AvatarURL, + }, + Created: from.CreatedAt, + Updated: from.UpdatedAt, + } +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/org.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/org.go new file mode 100644 index 00000000000..477f806c022 --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/org.go @@ -0,0 +1,84 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gitlab + +import ( + "context" + "fmt" + + "github.com/jenkins-x/go-scm/scm" + "github.com/jenkins-x/go-scm/scm/driver/internal/null" +) + +type organizationService struct { + client *wrapper +} + +func (s *organizationService) IsMember(ctx context.Context, org string, user string) (bool, *scm.Response, error) { + users, res, err := s.ListMemberUsers(ctx, org) + if err != nil { + return false, res, err + } + member := false + for _, u := range users { + if u.Login == user { + member = true + break + } + } + return member, res, err +} + +func (s *organizationService) ListTeams(ctx context.Context, org string, ops scm.ListOptions) ([]*scm.Team, *scm.Response, error) { + // TODO implement me + return nil, nil, nil +} + +func (s *organizationService) ListTeamMembers(ctx context.Context, id int, role string, ops scm.ListOptions) ([]*scm.TeamMember, *scm.Response, error) { + // TODO implement me + return nil, nil, nil +} + +func (s *organizationService) ListMemberUsers(ctx context.Context, org string) ([]scm.User, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/members/all", org) + out := []*user{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertUserList(out), res, err +} + +func (s *organizationService) Find(ctx context.Context, name string) (*scm.Organization, *scm.Response, error) { + path := fmt.Sprintf("api/v4/groups/%s", name) + out := new(organization) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertOrganization(out), res, err +} + +func (s *organizationService) List(ctx context.Context, opts scm.ListOptions) ([]*scm.Organization, *scm.Response, error) { + path := fmt.Sprintf("api/v4/groups?%s", encodeListOptions(opts)) + out := []*organization{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertOrganizationList(out), res, err +} + +type organization struct { + Name string `json:"name"` + Path string `json:"path"` + Avatar null.String `json:"avatar_url"` +} + +func convertOrganizationList(from []*organization) []*scm.Organization { + to := []*scm.Organization{} + for _, v := range from { + to = append(to, convertOrganization(v)) + } + return to +} + +func convertOrganization(from *organization) *scm.Organization { + return &scm.Organization{ + Name: from.Path, + Avatar: from.Avatar.String, + } +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/pr.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/pr.go new file mode 100644 index 00000000000..ff9e7b72cf3 --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/pr.go @@ -0,0 +1,216 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gitlab + +import ( + "context" + "fmt" + "net/url" + "strings" + "time" + + "github.com/jenkins-x/go-scm/scm" +) + +type pullService struct { + client *wrapper +} + +func (s *pullService) Find(ctx context.Context, repo string, number int) (*scm.PullRequest, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/merge_requests/%d", encode(repo), number) + out := new(pr) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertPullRequest(out), res, err +} + +func (s *pullService) FindComment(ctx context.Context, repo string, index, id int) (*scm.Comment, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/merge_requests/%d/notes/%d", encode(repo), index, id) + out := new(issueComment) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertIssueComment(out), res, err +} + +func (s *pullService) List(ctx context.Context, repo string, opts scm.PullRequestListOptions) ([]*scm.PullRequest, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/merge_requests?%s", encode(repo), encodePullRequestListOptions(opts)) + out := []*pr{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertPullRequestList(out), res, err +} + +func (s *pullService) ListChanges(ctx context.Context, repo string, number int, opts scm.ListOptions) ([]*scm.Change, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/merge_requests/%d/changes?%s", encode(repo), number, encodeListOptions(opts)) + out := new(changes) + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertChangeList(out.Changes), res, err +} + +func (s *pullService) ListComments(ctx context.Context, repo string, index int, opts scm.ListOptions) ([]*scm.Comment, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/merge_requests/%d/notes?%s", encode(repo), index, encodeListOptions(opts)) + out := []*issueComment{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertIssueCommentList(out), res, err +} + +func (s *pullService) ListLabels(ctx context.Context, repo string, number int, opts scm.ListOptions) ([]*scm.Label, *scm.Response, error) { + mr, _, err := s.Find(ctx, repo, number) + if err != nil { + return nil, nil, err + } + + return mr.Labels, nil, nil +} + +func (s *pullService) AddLabel(ctx context.Context, repo string, number int, label string) (*scm.Response, error) { + existingLabels, _, err := s.ListLabels(ctx, repo, number, scm.ListOptions{}) + if err != nil { + return nil, err + } + + allLabels := map[string]struct{}{} + for _, l := range existingLabels { + allLabels[l.Name] = struct{}{} + } + allLabels[label] = struct{}{} + + labelNames := []string{} + for l := range allLabels { + labelNames = append(labelNames, l) + } + + return s.setLabels(ctx, repo, number, labelNames) +} + +func (s *pullService) setLabels(ctx context.Context, repo string, number int, labels []string) (*scm.Response, error) { + in := url.Values{} + labelsStr := strings.Join(labels, ",") + in.Set("labels", labelsStr) + path := fmt.Sprintf("api/v4/projects/%s/merge_requests/%d?%s", encode(repo), number, in.Encode()) + + return s.client.do(ctx, "PUT", path, nil, nil) +} + +func (s *pullService) DeleteLabel(ctx context.Context, repo string, number int, label string) (*scm.Response, error) { + existingLabels, _, err := s.ListLabels(ctx, repo, number, scm.ListOptions{}) + if err != nil { + return nil, err + } + labels := []string{} + for _, l := range existingLabels { + if l.Name != label { + labels = append(labels, l.Name) + } + } + return s.setLabels(ctx, repo, number, labels) +} + +func (s *pullService) CreateComment(ctx context.Context, repo string, index int, input *scm.CommentInput) (*scm.Comment, *scm.Response, error) { + in := url.Values{} + in.Set("body", input.Body) + path := fmt.Sprintf("api/v4/projects/%s/merge_requests/%d/notes?%s", encode(repo), index, in.Encode()) + out := new(issueComment) + res, err := s.client.do(ctx, "POST", path, nil, out) + return convertIssueComment(out), res, err +} + +func (s *pullService) DeleteComment(ctx context.Context, repo string, index, id int) (*scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/merge_requests/%d/notes/%d", encode(repo), index, id) + res, err := s.client.do(ctx, "DELETE", path, nil, nil) + return res, err +} + +func (s *pullService) Merge(ctx context.Context, repo string, number int) (*scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/merge_requests/%d/merge", encode(repo), number) + res, err := s.client.do(ctx, "PUT", path, nil, nil) + return res, err +} + +func (s *pullService) Close(ctx context.Context, repo string, number int) (*scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/merge_requests/%d?state_event=closed", encode(repo), number) + res, err := s.client.do(ctx, "PUT", path, nil, nil) + return res, err +} + +type pr struct { + Number int `json:"iid"` + Sha string `json:"sha"` + Title string `json:"title"` + Desc string `json:"description"` + State string `json:"state"` + Link string `json:"web_url"` + Author struct { + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + Avatar string `json:"avatar_url"` + } + SourceBranch string `json:"source_branch"` + TargetBranch string `json:"target_branch"` + Created time.Time `json:"created_at"` + Updated time.Time `json:"updated_at"` + Closed time.Time +} + +type changes struct { + Changes []*change +} + +type change struct { + OldPath string `json:"old_path"` + NewPath string `json:"new_path"` + Added bool `json:"new_file"` + Renamed bool `json:"renamed_file"` + Deleted bool `json:"deleted_file"` +} + +func convertPullRequestList(from []*pr) []*scm.PullRequest { + to := []*scm.PullRequest{} + for _, v := range from { + to = append(to, convertPullRequest(v)) + } + return to +} + +func convertPullRequest(from *pr) *scm.PullRequest { + return &scm.PullRequest{ + Number: from.Number, + Title: from.Title, + Body: from.Desc, + Sha: from.Sha, + Ref: fmt.Sprintf("refs/merge-requests/%d/head", from.Number), + Source: from.SourceBranch, + Target: from.TargetBranch, + Link: from.Link, + Closed: from.State != "opened", + Merged: from.State == "merged", + Author: scm.User{ + Name: from.Author.Name, + Login: from.Author.Username, + Avatar: from.Author.Avatar, + }, + Created: from.Created, + Updated: from.Updated, + } +} + +func convertChangeList(from []*change) []*scm.Change { + to := []*scm.Change{} + for _, v := range from { + to = append(to, convertChange(v)) + } + return to +} + +func convertChange(from *change) *scm.Change { + to := &scm.Change{ + Path: from.NewPath, + Added: from.Added, + Deleted: from.Deleted, + Renamed: from.Renamed, + } + if to.Path == "" { + to.Path = from.OldPath + } + return to +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/repo.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/repo.go new file mode 100644 index 00000000000..df3f8153869 --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/repo.go @@ -0,0 +1,373 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gitlab + +import ( + "context" + "fmt" + "net/url" + "strconv" + "strings" + "time" + + "github.com/jenkins-x/go-scm/scm" + "github.com/jenkins-x/go-scm/scm/driver/internal/null" +) + +type repository struct { + ID int `json:"id"` + Path string `json:"path"` + PathNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Visibility string `json:"visibility"` + WebURL string `json:"web_url"` + SSHURL string `json:"ssh_url_to_repo"` + HTTPURL string `json:"http_url_to_repo"` + Namespace namespace `json:"namespace"` + Permissions permissions `json:"permissions"` +} + +type namespace struct { + Name string `json:"name"` + Path string `json:"path"` +} + +type permissions struct { + ProjectAccess access `json:"project_access"` + GroupAccess access `json:"group_access"` +} + +type access struct { + AccessLevel int `json:"access_level"` + NotificationLevel int `json:"notification_level"` +} + +type hook struct { + ID int `json:"id"` + URL string `json:"url"` + ProjectID int `json:"project_id"` + PushEvents bool `json:"push_events"` + IssuesEvents bool `json:"issues_events"` + MergeRequestsEvents bool `json:"merge_requests_events"` + TagPushEvents bool `json:"tag_push_events"` + NoteEvents bool `json:"note_events"` + JobEvents bool `json:"job_events"` + PipelineEvents bool `json:"pipeline_events"` + WikiPageEvents bool `json:"wiki_page_events"` + EnableSslVerification bool `json:"enable_ssl_verification"` + CreatedAt time.Time `json:"created_at"` +} + +type label struct { + ID int `json:"id"` + Name string `json:"name"` + Color string `json:"color"` + Description string `json:"description"` +} + +type repositoryService struct { + client *wrapper +} + +func (s *repositoryService) FindCombinedStatus(ctx context.Context, repo, ref string) (*scm.CombinedStatus, *scm.Response, error) { + panic("implement me") +} + +func (s *repositoryService) FindUserPermission(ctx context.Context, repo string, user string) (string, *scm.Response, error) { + panic("implement me") +} + +func (s *repositoryService) IsCollaborator(ctx context.Context, repo, user string) (bool, *scm.Response, error) { + users, resp, err := s.ListCollaborators(ctx, repo) + if err != nil { + return false, resp, err + } + for _, u := range users { + if u.Name == user || u.Login == user { + return true, resp, err + } + } + return false, resp, err +} + +func (s *repositoryService) ListCollaborators(ctx context.Context, repo string) ([]scm.User, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/members/all", encode(repo)) + out := []*user{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertUserList(out), res, err +} + +func (s *repositoryService) ListLabels(context.Context, string, scm.ListOptions) ([]*scm.Label, *scm.Response, error) { + panic("implement me") +} + +func (s *repositoryService) Find(ctx context.Context, repo string) (*scm.Repository, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s", encode(repo)) + out := new(repository) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertRepository(out), res, err +} + +func (s *repositoryService) FindHook(ctx context.Context, repo string, id string) (*scm.Hook, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/hooks/%s", encode(repo), id) + out := new(hook) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertHook(out), res, err +} + +func (s *repositoryService) FindPerms(ctx context.Context, repo string) (*scm.Perm, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s", encode(repo)) + out := new(repository) + res, err := s.client.do(ctx, "GET", path, nil, out) + return convertRepository(out).Perm, res, err +} + +func (s *repositoryService) List(ctx context.Context, opts scm.ListOptions) ([]*scm.Repository, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects?%s", encodeMemberListOptions(opts)) + out := []*repository{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertRepositoryList(out), res, err +} + +func (s *repositoryService) ListHooks(ctx context.Context, repo string, opts scm.ListOptions) ([]*scm.Hook, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/hooks?%s", encode(repo), encodeListOptions(opts)) + out := []*hook{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertHookList(out), res, err +} + +func (s *repositoryService) ListStatus(ctx context.Context, repo, ref string, opts scm.ListOptions) ([]*scm.Status, *scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/repository/commits/%s/statuses?%s", encode(repo), ref, encodeListOptions(opts)) + out := []*status{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + return convertStatusList(out), res, err +} + +func (s *repositoryService) CreateHook(ctx context.Context, repo string, input *scm.HookInput) (*scm.Hook, *scm.Response, error) { + params := url.Values{} + params.Set("url", input.Target) + if input.Secret != "" { + params.Set("token", input.Secret) + } + if input.SkipVerify { + params.Set("enable_ssl_verification", "true") + } + if input.Events.Branch { + // no-op + } + if input.Events.Issue { + params.Set("issues_events", "true") + } + if input.Events.IssueComment || + input.Events.PullRequestComment { + params.Set("note_events", "true") + } + if input.Events.PullRequest { + params.Set("merge_requests_events", "true") + } + if input.Events.Push || input.Events.Branch { + params.Set("push_events", "true") + } + if input.Events.Tag { + params.Set("tag_push_events", "true") + } + + path := fmt.Sprintf("api/v4/projects/%s/hooks?%s", encode(repo), params.Encode()) + out := new(hook) + res, err := s.client.do(ctx, "POST", path, nil, out) + return convertHook(out), res, err +} + +func (s *repositoryService) CreateStatus(ctx context.Context, repo, ref string, input *scm.StatusInput) (*scm.Status, *scm.Response, error) { + params := url.Values{} + params.Set("state", convertFromState(input.State)) + params.Set("name", input.Label) + params.Set("target_url", input.Target) + path := fmt.Sprintf("api/v4/projects/%s/statuses/%s?%s", encode(repo), ref, params.Encode()) + out := new(status) + res, err := s.client.do(ctx, "POST", path, nil, out) + return convertStatus(out), res, err +} + +func (s *repositoryService) DeleteHook(ctx context.Context, repo string, id string) (*scm.Response, error) { + path := fmt.Sprintf("api/v4/projects/%s/hooks/%s", encode(repo), id) + return s.client.do(ctx, "DELETE", path, nil, nil) +} + +// helper function to convert from the gogs repository list to +// the common repository structure. +func convertRepositoryList(from []*repository) []*scm.Repository { + to := []*scm.Repository{} + for _, v := range from { + to = append(to, convertRepository(v)) + } + return to +} + +// helper function to convert from the gogs repository structure +// to the common repository structure. +func convertRepository(from *repository) *scm.Repository { + to := &scm.Repository{ + ID: strconv.Itoa(from.ID), + Namespace: from.Namespace.Path, + Name: from.Path, + Branch: from.DefaultBranch, + Private: convertPrivate(from.Visibility), + Clone: from.HTTPURL, + CloneSSH: from.SSHURL, + Perm: &scm.Perm{ + Pull: true, + Push: canPush(from), + Admin: canAdmin(from), + }, + } + if to.Namespace == "" { + if parts := strings.SplitN(from.PathNamespace, "/", 2); len(parts) == 2 { + to.Namespace = parts[1] + } + } + return to +} + +func convertHookList(from []*hook) []*scm.Hook { + to := []*scm.Hook{} + for _, v := range from { + to = append(to, convertHook(v)) + } + return to +} + +func convertHook(from *hook) *scm.Hook { + return &scm.Hook{ + ID: strconv.Itoa(from.ID), + Active: true, + Target: from.URL, + Events: convertEvents(from), + SkipVerify: !from.EnableSslVerification, + } +} + +type status struct { + Name string `json:"name"` + Desc null.String `json:"description"` + Status string `json:"status"` + Sha string `json:"sha"` + Ref string `json:"ref"` + Target null.String `json:"target_url"` + Created time.Time `json:"created_at"` + Updated time.Time `json:"updated_at"` +} + +func convertStatusList(from []*status) []*scm.Status { + to := []*scm.Status{} + for _, v := range from { + to = append(to, convertStatus(v)) + } + return to +} + +func convertStatus(from *status) *scm.Status { + return &scm.Status{ + State: convertState(from.Status), + Label: from.Name, + Desc: from.Desc.String, + Target: from.Target.String, + } +} + +func convertEvents(from *hook) []string { + var events []string + if from.IssuesEvents { + events = append(events, "issues") + } + if from.TagPushEvents { + events = append(events, "tag") + } + if from.PushEvents { + events = append(events, "push") + } + if from.NoteEvents { + events = append(events, "comment") + } + if from.MergeRequestsEvents { + events = append(events, "merge") + } + return events +} + +func convertState(from string) scm.State { + switch from { + case "canceled": + return scm.StateCanceled + case "failed": + return scm.StateFailure + case "pending": + return scm.StatePending + case "running": + return scm.StateRunning + case "success": + return scm.StateSuccess + default: + return scm.StateUnknown + } +} + +func convertFromState(from scm.State) string { + switch from { + case scm.StatePending: + return "pending" + case scm.StateRunning: + return "running" + case scm.StateSuccess: + return "success" + case scm.StateCanceled: + return "canceled" + default: + return "failed" + } +} + +func convertPrivate(from string) bool { + switch from { + case "public", "": + return false + default: + return true + } +} + +func convertLabelObjects(from []*label) []*scm.Label { + var labels []*scm.Label + for _, label := range from { + labels = append(labels, &scm.Label{ + Name: label.Name, + Description: label.Description, + Color: label.Color, + }) + } + return labels +} + +func canPush(proj *repository) bool { + switch { + case proj.Permissions.ProjectAccess.AccessLevel >= 30: + return true + case proj.Permissions.GroupAccess.AccessLevel >= 30: + return true + default: + return false + } +} + +func canAdmin(proj *repository) bool { + switch { + case proj.Permissions.ProjectAccess.AccessLevel >= 40: + return true + case proj.Permissions.GroupAccess.AccessLevel >= 40: + return true + default: + return false + } +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/review.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/review.go new file mode 100644 index 00000000000..3baf4346fca --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/review.go @@ -0,0 +1,31 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gitlab + +import ( + "context" + + "github.com/jenkins-x/go-scm/scm" +) + +type reviewService struct { + client *wrapper +} + +func (s *reviewService) Find(ctx context.Context, repo string, number, id int) (*scm.Review, *scm.Response, error) { + return nil, nil, scm.ErrNotSupported +} + +func (s *reviewService) List(ctx context.Context, repo string, number int, opts scm.ListOptions) ([]*scm.Review, *scm.Response, error) { + return nil, nil, scm.ErrNotSupported +} + +func (s *reviewService) Create(ctx context.Context, repo string, number int, input *scm.ReviewInput) (*scm.Review, *scm.Response, error) { + return nil, nil, scm.ErrNotSupported +} + +func (s *reviewService) Delete(ctx context.Context, repo string, number, id int) (*scm.Response, error) { + return nil, scm.ErrNotSupported +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/user.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/user.go new file mode 100644 index 00000000000..ef9883bba2c --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/user.go @@ -0,0 +1,66 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gitlab + +import ( + "context" + "fmt" + "strings" + + "github.com/jenkins-x/go-scm/scm" + "github.com/jenkins-x/go-scm/scm/driver/internal/null" +) + +type userService struct { + client *wrapper +} + +func (s *userService) Find(ctx context.Context) (*scm.User, *scm.Response, error) { + out := new(user) + res, err := s.client.do(ctx, "GET", "api/v4/user", nil, out) + return convertUser(out), res, err +} + +func (s *userService) FindLogin(ctx context.Context, login string) (*scm.User, *scm.Response, error) { + path := fmt.Sprintf("api/v4/users?search=%s", login) + out := []*user{} + res, err := s.client.do(ctx, "GET", path, nil, &out) + if err != nil { + return nil, nil, err + } + if len(out) != 1 || !strings.EqualFold(out[0].Username, login) { + return nil, nil, scm.ErrNotFound + } + return convertUser(out[0]), res, err +} + +func (s *userService) FindEmail(ctx context.Context) (string, *scm.Response, error) { + user, res, err := s.Find(ctx) + return user.Email, res, err +} + +type user struct { + Username string `json:"username"` + Name string `json:"name"` + Email null.String `json:"email"` + Avatar string `json:"avatar_url"` +} + +func convertUser(from *user) *scm.User { + return &scm.User{ + Avatar: from.Avatar, + Email: from.Email.String, + Login: from.Username, + Name: from.Name, + } +} + +func convertUserList(users []*user) []scm.User { + dst := []scm.User{} + for _, src := range users { + dst = append(dst, *convertUser(src)) + } + return dst +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/util.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/util.go new file mode 100644 index 00000000000..b964e0169af --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/util.go @@ -0,0 +1,90 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gitlab + +import ( + "net/url" + "strconv" + "strings" + + "github.com/jenkins-x/go-scm/scm" +) + +func encode(s string) string { + return strings.Replace(s, "/", "%2F", -1) +} + +func encodeListOptions(opts scm.ListOptions) string { + params := url.Values{} + if opts.Page != 0 { + params.Set("page", strconv.Itoa(opts.Page)) + } + if opts.Size != 0 { + params.Set("per_page", strconv.Itoa(opts.Size)) + } + return params.Encode() +} + +func encodeMemberListOptions(opts scm.ListOptions) string { + params := url.Values{} + params.Set("membership", "true") + if opts.Page != 0 { + params.Set("page", strconv.Itoa(opts.Page)) + } + if opts.Size != 0 { + params.Set("per_page", strconv.Itoa(opts.Size)) + } + return params.Encode() +} + +func encodeCommitListOptions(opts scm.CommitListOptions) string { + params := url.Values{} + if opts.Page != 0 { + params.Set("page", strconv.Itoa(opts.Page)) + } + if opts.Size != 0 { + params.Set("per_page", strconv.Itoa(opts.Size)) + } + if opts.Ref != "" { + params.Set("ref_name", opts.Ref) + } + return params.Encode() +} + +func encodeIssueListOptions(opts scm.IssueListOptions) string { + params := url.Values{} + if opts.Page != 0 { + params.Set("page", strconv.Itoa(opts.Page)) + } + if opts.Size != 0 { + params.Set("per_page", strconv.Itoa(opts.Size)) + } + if opts.Open && opts.Closed { + params.Set("state", "all") + } else if opts.Closed { + params.Set("state", "closed") + } else if opts.Open { + params.Set("state", "opened") + } + return params.Encode() +} + +func encodePullRequestListOptions(opts scm.PullRequestListOptions) string { + params := url.Values{} + if opts.Page != 0 { + params.Set("page", strconv.Itoa(opts.Page)) + } + if opts.Size != 0 { + params.Set("per_page", strconv.Itoa(opts.Size)) + } + if opts.Open && opts.Closed { + params.Set("state", "all") + } else if opts.Closed { + params.Set("state", "closed") + } else if opts.Open { + params.Set("state", "opened") + } + return params.Encode() +} diff --git a/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/webhook.go b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/webhook.go new file mode 100644 index 00000000000..e9efa9f15bb --- /dev/null +++ b/vendor/github.com/jenkins-x/go-scm/scm/driver/gitlab/webhook.go @@ -0,0 +1,786 @@ +// Copyright 2017 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gitlab + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "strconv" + + "github.com/jenkins-x/go-scm/scm" +) + +type webhookService struct { + client *wrapper +} + +func (s *webhookService) Parse(req *http.Request, fn scm.SecretFunc) (scm.Webhook, error) { + data, err := ioutil.ReadAll( + io.LimitReader(req.Body, 10000000), + ) + if err != nil { + return nil, err + } + + var hook scm.Webhook + event := req.Header.Get("X-Gitlab-Event") + switch event { + case "Push Hook", "Tag Push Hook": + hook, err = parsePushHook(data) + case "Issue Hook": + return nil, scm.UnknownWebhook{event} + case "Merge Request Hook": + hook, err = parsePullRequestHook(data) + default: + return nil, scm.UnknownWebhook{event} + } + if err != nil { + return nil, err + } + + // get the gitlab shared token to verify the payload + // authenticity. If no key is provided, no validation + // is performed. + token, err := fn(hook) + if err != nil { + return hook, err + } else if token == "" { + return hook, nil + } + + if token != req.Header.Get("X-Gitlab-Token") { + return hook, scm.ErrSignatureInvalid + } + + return hook, nil +} + +func parsePushHook(data []byte) (scm.Webhook, error) { + src := new(pushHook) + err := json.Unmarshal(data, src) + if err != nil { + return nil, err + } + switch { + case src.ObjectKind == "push" && src.Before == "0000000000000000000000000000000000000000": + // TODO we previously considered returning a + // branch creation hook, however, the push hook + // returns more metadata (commit details). + return convertPushHook(src), nil + case src.ObjectKind == "push" && src.After == "0000000000000000000000000000000000000000": + return converBranchHook(src), nil + case src.ObjectKind == "tag_push" && src.Before == "0000000000000000000000000000000000000000": + // TODO we previously considered returning a + // tag creation hook, however, the push hook + // returns more metadata (commit details). + return convertPushHook(src), nil + case src.ObjectKind == "tag_push" && src.After == "0000000000000000000000000000000000000000": + return convertTagHook(src), nil + default: + return convertPushHook(src), nil + } +} + +func parsePullRequestHook(data []byte) (scm.Webhook, error) { + src := new(pullRequestHook) + err := json.Unmarshal(data, src) + if err != nil { + return nil, err + } + if src.ObjectAttributes.Action == "" { + src.ObjectAttributes.Action = "open" + } + event := src.ObjectAttributes.Action + switch event { + case "open", "close", "reopen", "merge", "update": + // no-op + default: + return nil, scm.UnknownWebhook{event} + } + switch { + default: + return convertPullRequestHook(src), nil + } +} + +func convertPushHook(src *pushHook) *scm.PushHook { + namespace, name := scm.Split(src.Project.PathWithNamespace) + dst := &scm.PushHook{ + Ref: scm.ExpandRef(src.Ref, "refs/heads/"), + Repo: scm.Repository{ + ID: strconv.Itoa(src.Project.ID), + Namespace: namespace, + Name: name, + Clone: src.Project.GitHTTPURL, + CloneSSH: src.Project.GitSSHURL, + Link: src.Project.WebURL, + Branch: src.Project.DefaultBranch, + Private: false, // TODO how do we correctly set Private vs Public? + }, + Commit: scm.Commit{ + Sha: src.CheckoutSha, + Message: "", // NOTE this is set below + Author: scm.Signature{ + Login: src.UserUsername, + Name: src.UserName, + Email: src.UserEmail, + Avatar: src.UserAvatar, + }, + Committer: scm.Signature{ + Login: src.UserUsername, + Name: src.UserName, + Email: src.UserEmail, + Avatar: src.UserAvatar, + }, + Link: "", // NOTE this is set below + }, + Sender: scm.User{ + Login: src.UserUsername, + Name: src.UserName, + Email: src.UserEmail, + Avatar: src.UserAvatar, + }, + } + if len(src.Commits) > 0 { + dst.Commit.Message = src.Commits[0].Message + dst.Commit.Link = src.Commits[0].URL + } + return dst +} + +func converBranchHook(src *pushHook) *scm.BranchHook { + action := scm.ActionCreate + commit := src.After + if src.After == "0000000000000000000000000000000000000000" { + action = scm.ActionDelete + commit = src.Before + } + namespace, name := scm.Split(src.Project.PathWithNamespace) + return &scm.BranchHook{ + Action: action, + Ref: scm.Reference{ + Name: scm.TrimRef(src.Ref), + Sha: commit, + }, + Repo: scm.Repository{ + ID: strconv.Itoa(src.Project.ID), + Namespace: namespace, + Name: name, + Clone: src.Project.GitHTTPURL, + CloneSSH: src.Project.GitSSHURL, + Link: src.Project.WebURL, + Branch: src.Project.DefaultBranch, + Private: false, // TODO how do we correctly set Private vs Public? + }, + Sender: scm.User{ + Login: src.UserUsername, + Name: src.UserName, + Email: src.UserEmail, + Avatar: src.UserAvatar, + }, + } +} + +func convertTagHook(src *pushHook) *scm.TagHook { + action := scm.ActionCreate + commit := src.After + if src.After == "0000000000000000000000000000000000000000" { + action = scm.ActionDelete + commit = src.Before + } + namespace, name := scm.Split(src.Project.PathWithNamespace) + return &scm.TagHook{ + Action: action, + Ref: scm.Reference{ + Name: scm.TrimRef(src.Ref), + Sha: commit, + }, + Repo: scm.Repository{ + ID: strconv.Itoa(src.Project.ID), + Namespace: namespace, + Name: name, + Clone: src.Project.GitHTTPURL, + CloneSSH: src.Project.GitSSHURL, + Link: src.Project.WebURL, + Branch: src.Project.DefaultBranch, + Private: false, // TODO how do we correctly set Private vs Public? + }, + Sender: scm.User{ + Login: src.UserUsername, + Name: src.UserName, + Email: src.UserEmail, + Avatar: src.UserAvatar, + }, + } +} + +func convertPullRequestHook(src *pullRequestHook) *scm.PullRequestHook { + action := scm.ActionSync + switch src.ObjectAttributes.Action { + case "open": + action = scm.ActionOpen + case "close": + action = scm.ActionClose + case "reopen": + action = scm.ActionReopen + case "merge": + action = scm.ActionMerge + case "update": + action = scm.ActionSync + } + fork := scm.Join( + src.ObjectAttributes.Source.Namespace, + src.ObjectAttributes.Source.Name, + ) + namespace, name := scm.Split(src.Project.PathWithNamespace) + repo := scm.Repository{ + ID: strconv.Itoa(src.Project.ID), + Namespace: namespace, + Name: name, + Clone: src.Project.GitHTTPURL, + CloneSSH: src.Project.GitSSHURL, + Link: src.Project.WebURL, + Branch: src.Project.DefaultBranch, + Private: false, // TODO how do we correctly set Private vs Public? + } + ref := fmt.Sprintf("refs/merge-requests/%d/head", src.ObjectAttributes.Iid) + sha := src.ObjectAttributes.LastCommit.ID + pr := scm.PullRequest{ + Number: src.ObjectAttributes.Iid, + Title: src.ObjectAttributes.Title, + Body: src.ObjectAttributes.Description, + Sha: sha, + Ref: ref, + Base: scm.PullRequestBranch{ + Ref: repo.Branch, + }, + Head: scm.PullRequestBranch{ + Sha: sha, + }, + Source: src.ObjectAttributes.SourceBranch, + Target: src.ObjectAttributes.TargetBranch, + Fork: fork, + Link: src.ObjectAttributes.URL, + Closed: src.ObjectAttributes.State != "opened", + Merged: src.ObjectAttributes.State == "merged", + // Created : src.ObjectAttributes.CreatedAt, + // Updated : src.ObjectAttributes.UpdatedAt, // 2017-12-10 17:01:11 UTC + Author: scm.User{ + Login: src.User.Username, + Name: src.User.Name, + Email: "", // TODO how do we get the pull request author email? + Avatar: src.User.AvatarURL, + }, + } + pr.Base.Repo = repo + pr.Head.Repo = repo + return &scm.PullRequestHook{ + Action: action, + PullRequest: pr, + Repo: repo, + Sender: scm.User{ + Login: src.User.Username, + Name: src.User.Name, + Email: "", // TODO how do we get the pull request author email? + Avatar: src.User.AvatarURL, + }, + } +} + +type ( + pushHook struct { + ObjectKind string `json:"object_kind"` + EventName string `json:"event_name"` + Before string `json:"before"` + After string `json:"after"` + Ref string `json:"ref"` + CheckoutSha string `json:"checkout_sha"` + Message interface{} `json:"message"` + UserID int `json:"user_id"` + UserName string `json:"user_name"` + UserUsername string `json:"user_username"` + UserEmail string `json:"user_email"` + UserAvatar string `json:"user_avatar"` + ProjectID int `json:"project_id"` + Project struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL interface{} `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + CiConfigPath interface{} `json:"ci_config_path"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"project"` + Commits []struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + Added []string `json:"added"` + Modified []interface{} `json:"modified"` + Removed []interface{} `json:"removed"` + } `json:"commits"` + TotalCommitsCount int `json:"total_commits_count"` + Repository struct { + Name string `json:"name"` + URL string `json:"url"` + Description string `json:"description"` + Homepage string `json:"homepage"` + GitHTTPURL string `json:"git_http_url"` + GitSSHURL string `json:"git_ssh_url"` + VisibilityLevel int `json:"visibility_level"` + } `json:"repository"` + } + + commentHook struct { + ObjectKind string `json:"object_kind"` + User struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"user"` + ProjectID int `json:"project_id"` + Project struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL interface{} `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + CiConfigPath interface{} `json:"ci_config_path"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"project"` + ObjectAttributes struct { + ID int `json:"id"` + Note string `json:"note"` + NoteableType string `json:"noteable_type"` + AuthorID int `json:"author_id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ProjectID int `json:"project_id"` + Attachment interface{} `json:"attachment"` + LineCode string `json:"line_code"` + CommitID string `json:"commit_id"` + NoteableID int `json:"noteable_id"` + StDiff interface{} `json:"st_diff"` + System bool `json:"system"` + UpdatedByID interface{} `json:"updated_by_id"` + Type string `json:"type"` + Position struct { + BaseSha string `json:"base_sha"` + StartSha string `json:"start_sha"` + HeadSha string `json:"head_sha"` + OldPath string `json:"old_path"` + NewPath string `json:"new_path"` + PositionType string `json:"position_type"` + OldLine interface{} `json:"old_line"` + NewLine int `json:"new_line"` + } `json:"position"` + OriginalPosition struct { + BaseSha string `json:"base_sha"` + StartSha string `json:"start_sha"` + HeadSha string `json:"head_sha"` + OldPath string `json:"old_path"` + NewPath string `json:"new_path"` + PositionType string `json:"position_type"` + OldLine interface{} `json:"old_line"` + NewLine int `json:"new_line"` + } `json:"original_position"` + ResolvedAt interface{} `json:"resolved_at"` + ResolvedByID interface{} `json:"resolved_by_id"` + DiscussionID string `json:"discussion_id"` + ChangePosition struct { + BaseSha interface{} `json:"base_sha"` + StartSha interface{} `json:"start_sha"` + HeadSha interface{} `json:"head_sha"` + OldPath interface{} `json:"old_path"` + NewPath interface{} `json:"new_path"` + PositionType string `json:"position_type"` + OldLine interface{} `json:"old_line"` + NewLine interface{} `json:"new_line"` + } `json:"change_position"` + ResolvedByPush interface{} `json:"resolved_by_push"` + URL string `json:"url"` + } `json:"object_attributes"` + Repository struct { + Name string `json:"name"` + URL string `json:"url"` + Description string `json:"description"` + Homepage string `json:"homepage"` + } `json:"repository"` + MergeRequest struct { + AssigneeID interface{} `json:"assignee_id"` + AuthorID int `json:"author_id"` + CreatedAt string `json:"created_at"` + DeletedAt interface{} `json:"deleted_at"` + Description string `json:"description"` + HeadPipelineID interface{} `json:"head_pipeline_id"` + ID int `json:"id"` + Iid int `json:"iid"` + LastEditedAt interface{} `json:"last_edited_at"` + LastEditedByID interface{} `json:"last_edited_by_id"` + MergeCommitSha interface{} `json:"merge_commit_sha"` + MergeError interface{} `json:"merge_error"` + MergeParams struct { + ForceRemoveSourceBranch string `json:"force_remove_source_branch"` + } `json:"merge_params"` + MergeStatus string `json:"merge_status"` + MergeUserID interface{} `json:"merge_user_id"` + MergeWhenPipelineSucceeds bool `json:"merge_when_pipeline_succeeds"` + MilestoneID interface{} `json:"milestone_id"` + SourceBranch string `json:"source_branch"` + SourceProjectID int `json:"source_project_id"` + State string `json:"state"` + TargetBranch string `json:"target_branch"` + TargetProjectID int `json:"target_project_id"` + TimeEstimate int `json:"time_estimate"` + Title string `json:"title"` + UpdatedAt string `json:"updated_at"` + UpdatedByID interface{} `json:"updated_by_id"` + URL string `json:"url"` + Source struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL interface{} `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + CiConfigPath interface{} `json:"ci_config_path"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"source"` + Target struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL interface{} `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + CiConfigPath interface{} `json:"ci_config_path"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"target"` + LastCommit struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + } `json:"last_commit"` + WorkInProgress bool `json:"work_in_progress"` + TotalTimeSpent int `json:"total_time_spent"` + HumanTotalTimeSpent interface{} `json:"human_total_time_spent"` + HumanTimeEstimate interface{} `json:"human_time_estimate"` + } `json:"merge_request"` + } + + tagHook struct { + ObjectKind string `json:"object_kind"` + EventName string `json:"event_name"` + Before string `json:"before"` + After string `json:"after"` + Ref string `json:"ref"` + CheckoutSha string `json:"checkout_sha"` + Message interface{} `json:"message"` + UserID int `json:"user_id"` + UserName string `json:"user_name"` + UserUsername string `json:"user_username"` + UserEmail string `json:"user_email"` + UserAvatar string `json:"user_avatar"` + ProjectID int `json:"project_id"` + Project struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL interface{} `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + CiConfigPath interface{} `json:"ci_config_path"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"project"` + Commits []struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + Added []string `json:"added"` + Modified []interface{} `json:"modified"` + Removed []interface{} `json:"removed"` + } `json:"commits"` + TotalCommitsCount int `json:"total_commits_count"` + Repository struct { + Name string `json:"name"` + URL string `json:"url"` + Description string `json:"description"` + Homepage string `json:"homepage"` + GitHTTPURL string `json:"git_http_url"` + GitSSHURL string `json:"git_ssh_url"` + VisibilityLevel int `json:"visibility_level"` + } `json:"repository"` + } + + issueHook struct { + ObjectKind string `json:"object_kind"` + User struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"user"` + Project struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL interface{} `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + CiConfigPath interface{} `json:"ci_config_path"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"project"` + ObjectAttributes struct { + AssigneeID interface{} `json:"assignee_id"` + AuthorID int `json:"author_id"` + BranchName interface{} `json:"branch_name"` + ClosedAt interface{} `json:"closed_at"` + Confidential bool `json:"confidential"` + CreatedAt string `json:"created_at"` + DeletedAt interface{} `json:"deleted_at"` + Description string `json:"description"` + DueDate interface{} `json:"due_date"` + ID int `json:"id"` + Iid int `json:"iid"` + LastEditedAt string `json:"last_edited_at"` + LastEditedByID int `json:"last_edited_by_id"` + MilestoneID interface{} `json:"milestone_id"` + MovedToID interface{} `json:"moved_to_id"` + ProjectID int `json:"project_id"` + RelativePosition int `json:"relative_position"` + State string `json:"state"` + TimeEstimate int `json:"time_estimate"` + Title string `json:"title"` + UpdatedAt string `json:"updated_at"` + UpdatedByID int `json:"updated_by_id"` + URL string `json:"url"` + TotalTimeSpent int `json:"total_time_spent"` + HumanTotalTimeSpent interface{} `json:"human_total_time_spent"` + HumanTimeEstimate interface{} `json:"human_time_estimate"` + AssigneeIds []interface{} `json:"assignee_ids"` + Action string `json:"action"` + } `json:"object_attributes"` + Labels []struct { + ID int `json:"id"` + Title string `json:"title"` + Color string `json:"color"` + ProjectID int `json:"project_id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Template bool `json:"template"` + Description string `json:"description"` + Type string `json:"type"` + GroupID interface{} `json:"group_id"` + } `json:"labels"` + Changes struct { + Labels struct { + Previous []interface{} `json:"previous"` + Current []struct { + ID int `json:"id"` + Title string `json:"title"` + Color string `json:"color"` + ProjectID int `json:"project_id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Template bool `json:"template"` + Description string `json:"description"` + Type string `json:"type"` + GroupID interface{} `json:"group_id"` + } `json:"current"` + } `json:"labels"` + } `json:"changes"` + Repository struct { + Name string `json:"name"` + URL string `json:"url"` + Description string `json:"description"` + Homepage string `json:"homepage"` + } `json:"repository"` + } + + pullRequestHook struct { + ObjectKind string `json:"object_kind"` + User struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"user"` + Project struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL interface{} `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + CiConfigPath interface{} `json:"ci_config_path"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"project"` + ObjectAttributes struct { + AssigneeID interface{} `json:"assignee_id"` + AuthorID int `json:"author_id"` + CreatedAt string `json:"created_at"` + DeletedAt interface{} `json:"deleted_at"` + Description string `json:"description"` + HeadPipelineID interface{} `json:"head_pipeline_id"` + ID int `json:"id"` + Iid int `json:"iid"` + LastEditedAt interface{} `json:"last_edited_at"` + LastEditedByID interface{} `json:"last_edited_by_id"` + MergeCommitSha interface{} `json:"merge_commit_sha"` + MergeError interface{} `json:"merge_error"` + MergeParams struct { + ForceRemoveSourceBranch string `json:"force_remove_source_branch"` + } `json:"merge_params"` + MergeStatus string `json:"merge_status"` + MergeUserID interface{} `json:"merge_user_id"` + MergeWhenPipelineSucceeds bool `json:"merge_when_pipeline_succeeds"` + MilestoneID interface{} `json:"milestone_id"` + SourceBranch string `json:"source_branch"` + SourceProjectID int `json:"source_project_id"` + State string `json:"state"` + TargetBranch string `json:"target_branch"` + TargetProjectID int `json:"target_project_id"` + TimeEstimate int `json:"time_estimate"` + Title string `json:"title"` + UpdatedAt string `json:"updated_at"` + UpdatedByID interface{} `json:"updated_by_id"` + URL string `json:"url"` + Source struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL interface{} `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + CiConfigPath interface{} `json:"ci_config_path"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"source"` + Target struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL interface{} `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + VisibilityLevel int `json:"visibility_level"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + CiConfigPath interface{} `json:"ci_config_path"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + } `json:"target"` + LastCommit struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + } `json:"last_commit"` + WorkInProgress bool `json:"work_in_progress"` + TotalTimeSpent int `json:"total_time_spent"` + HumanTotalTimeSpent interface{} `json:"human_total_time_spent"` + HumanTimeEstimate interface{} `json:"human_time_estimate"` + Action string `json:"action"` + } `json:"object_attributes"` + Labels []interface{} `json:"labels"` + Changes struct { + } `json:"changes"` + Repository struct { + Name string `json:"name"` + URL string `json:"url"` + Description string `json:"description"` + Homepage string `json:"homepage"` + } `json:"repository"` + } +) diff --git a/vendor/github.com/jenkins-x/go-scm/scm/pr.go b/vendor/github.com/jenkins-x/go-scm/scm/pr.go index b93488e13dd..a890fee2175 100644 --- a/vendor/github.com/jenkins-x/go-scm/scm/pr.go +++ b/vendor/github.com/jenkins-x/go-scm/scm/pr.go @@ -98,6 +98,9 @@ type ( // ListComments returns the pull request comment list. ListComments(context.Context, string, int, ListOptions) ([]*Comment, *Response, error) + // ListLabels returns the labels on a pull request + ListLabels(context.Context, string, int, ListOptions) ([]*Label, *Response, error) + // Merge merges the repository pull request. Merge(context.Context, string, int) (*Response, error) @@ -109,6 +112,12 @@ type ( // DeleteComment deletes an pull request comment. DeleteComment(context.Context, string, int, int) (*Response, error) + + // AddLabel adds a label to a pull request. + AddLabel(ctx context.Context, repo string, number int, label string) (*Response, error) + + // DeleteLabel deletes a label from a pull request + DeleteLabel(ctx context.Context, repo string, number int, label string) (*Response, error) } ) diff --git a/vendor/github.com/jenkins-x/go-scm/scm/webhook.go b/vendor/github.com/jenkins-x/go-scm/scm/webhook.go index 42df4ba1c4d..39cc1d689b9 100644 --- a/vendor/github.com/jenkins-x/go-scm/scm/webhook.go +++ b/vendor/github.com/jenkins-x/go-scm/scm/webhook.go @@ -12,17 +12,23 @@ import ( type WebhookKind string const ( - WebhookKindPing WebhookKind = "ping" - WebhookKindPush WebhookKind = "push" WebhookKindBranch WebhookKind = "branch" + WebhookKindCheckRun WebhookKind = "check_run" + WebhookKindCheckSuite WebhookKind = "check_suite" WebhookKindDeploy WebhookKind = "deploy" - WebhookKindTag WebhookKind = "tag" + WebhookKindDeploymentStatus WebhookKind = "deployment_status" + WebhookKindInstallation WebhookKind = "installation" WebhookKindIssue WebhookKind = "issue" WebhookKindIssueComment WebhookKind = "issue_comment" + WebhookKindLabel WebhookKind = "label" + WebhookKindPing WebhookKind = "ping" WebhookKindPullRequest WebhookKind = "pull_request" WebhookKindPullRequestComment WebhookKind = "pull_request_comment" + WebhookKindPush WebhookKind = "push" + WebhookKindRelease WebhookKind = "release" WebhookKindReviewCommentHook WebhookKind = "review_comment" - WebhookKindInstallation WebhookKind = "installation" + WebhookKindStatus WebhookKind = "status" + WebhookKindTag WebhookKind = "tag" ) var ( @@ -41,6 +47,7 @@ type ( // Label on a PR Label struct { + ID int64 URL string Name string Description string @@ -92,6 +99,33 @@ type ( Installation *InstallationRef } + // CheckRunHook represents a check run event + CheckRunHook struct { + Action Action + Repo Repository + Sender User + Label Label + Installation *InstallationRef + } + + // CheckSuiteHook represents a check suite event + CheckSuiteHook struct { + Action Action + Repo Repository + Sender User + Label Label + Installation *InstallationRef + } + + // DeploymentStatusHook represents a check suite event + DeploymentStatusHook struct { + Action Action + Repo Repository + Sender User + Label Label + Installation *InstallationRef + } + // TagHook represents a tag event, eg create and delete // github event types. TagHook struct { @@ -136,6 +170,33 @@ type ( NodeID string } + // LabelHook represents a label event + LabelHook struct { + Action Action + Repo Repository + Sender User + Label Label + Installation *InstallationRef + } + + // ReleaseHook represents a relese event + ReleaseHook struct { + Action Action + Repo Repository + Sender User + Label Label + Installation *InstallationRef + } + + // StatusHook represents a status event + StatusHook struct { + Action Action + Repo Repository + Sender User + Label Label + Installation *InstallationRef + } + // Account represents the account of a GitHub app install Account struct { ID int @@ -230,6 +291,12 @@ func (h *PullRequestHook) Kind() WebhookKind { return WebhookKindPullRequ func (h *PullRequestCommentHook) Kind() WebhookKind { return WebhookKindPullRequestComment } func (h *ReviewCommentHook) Kind() WebhookKind { return WebhookKindReviewCommentHook } func (h *InstallationHook) Kind() WebhookKind { return WebhookKindInstallation } +func (h *LabelHook) Kind() WebhookKind { return WebhookKindLabel } +func (h *StatusHook) Kind() WebhookKind { return WebhookKindStatus } +func (h *CheckRunHook) Kind() WebhookKind { return WebhookKindCheckRun } +func (h *CheckSuiteHook) Kind() WebhookKind { return WebhookKindCheckSuite } +func (h *DeploymentStatusHook) Kind() WebhookKind { return WebhookKindDeploymentStatus } +func (h *ReleaseHook) Kind() WebhookKind { return WebhookKindRelease } // Repository() defines the repository webhook and provides // a convenient way to get the associated repository without @@ -245,6 +312,12 @@ func (h *IssueCommentHook) Repository() Repository { return h.Repo } func (h *PullRequestHook) Repository() Repository { return h.Repo } func (h *PullRequestCommentHook) Repository() Repository { return h.Repo } func (h *ReviewCommentHook) Repository() Repository { return h.Repo } +func (h *LabelHook) Repository() Repository { return h.Repo } +func (h *StatusHook) Repository() Repository { return h.Repo } +func (h *CheckRunHook) Repository() Repository { return h.Repo } +func (h *CheckSuiteHook) Repository() Repository { return h.Repo } +func (h *DeploymentStatusHook) Repository() Repository { return h.Repo } +func (h *ReleaseHook) Repository() Repository { return h.Repo } func (h *InstallationHook) Repository() Repository { if len(h.Repos) > 0 { @@ -265,6 +338,12 @@ func (h *IssueCommentHook) GetInstallationRef() *InstallationRef { return func (h *PullRequestHook) GetInstallationRef() *InstallationRef { return h.Installation } func (h *PullRequestCommentHook) GetInstallationRef() *InstallationRef { return h.Installation } func (h *ReviewCommentHook) GetInstallationRef() *InstallationRef { return h.Installation } +func (h *LabelHook) GetInstallationRef() *InstallationRef { return h.Installation } +func (h *StatusHook) GetInstallationRef() *InstallationRef { return h.Installation } +func (h *CheckRunHook) GetInstallationRef() *InstallationRef { return h.Installation } +func (h *CheckSuiteHook) GetInstallationRef() *InstallationRef { return h.Installation } +func (h *DeploymentStatusHook) GetInstallationRef() *InstallationRef { return h.Installation } +func (h *ReleaseHook) GetInstallationRef() *InstallationRef { return h.Installation } func (h *InstallationHook) GetInstallationRef() *InstallationRef { if h.Installation == nil {