diff --git a/api/.gitignore b/api/.gitignore index daf913b..0c9e329 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -6,6 +6,7 @@ # Folders _obj _test +.idea # Architecture specific extensions/prefixes *.[568vq] diff --git a/api/.travis.yml b/api/.travis.yml index 82d1952..09075b6 100644 --- a/api/.travis.yml +++ b/api/.travis.yml @@ -1,5 +1,27 @@ language: go go: - - 1.4.3 - - 1.5.1 + - 1.9.x + - 1.10.x + - 1.11.x + - master + +stages: + - lint + - test + +jobs: + include: + - stage: lint + script: + - go get golang.org/x/lint/golint + - golint -set_exit_status + - go vet -v + - stage: test + script: + - go test -v + +matrix: + allow_failures: + - go: master + fast_finish: true diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 2f3f7de..29e93ff 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -1,6 +1,27 @@ go-github CHANGELOG =================== +0.6.0 +----- +- Add support for the V4 Gitlab API. This means the older V3 API is no longer fully supported + with this version. If you still need that version, please use the `f-api-v3` branch. + +0.4.0 +----- +- Add support to use [`sudo`](https://docs.gitlab.com/ce/api/README.html#sudo) for all API calls. +- Add support for the Notification Settings API. +- Add support for the Time Tracking API. +- Make sure that the error response correctly outputs any returned errors. +- And a reasonable number of smaller enhanchements and bugfixes. + +0.3.0 +----- +- Moved the tags related API calls to their own service, following the Gitlab API structure. + +0.2.0 +----- +- Convert all Option structs to use pointers for their fields. + 0.1.0 ----- -- Initial release +- Initial release. diff --git a/api/README.md b/api/README.md index a953680..fb03398 100644 --- a/api/README.md +++ b/api/README.md @@ -2,33 +2,87 @@ A GitLab API client enabling Go programs to interact with GitLab in a simple and uniform way -**Documentation:** [![GoDoc](https://godoc.org/github.com/xanzy/go-gitlab?status.svg)](https://godoc.org/github.com/xanzy/go-gitlab) -**Build Status:** [![Build Status](https://travis-ci.org/xanzy/go-gitlab.svg?branch=master)](https://travis-ci.org/xanzy/go-gitlab) +[![Build Status](https://travis-ci.org/xanzy/go-gitlab.svg?branch=master)](https://travis-ci.org/xanzy/go-gitlab) +[![GitHub license](https://img.shields.io/github/license/xanzy/go-gitlab.svg)](https://github.com/xanzy/go-gitlab/blob/master/LICENSE) +[![Sourcegraph](https://sourcegraph.com/github.com/xanzy/go-gitlab/-/badge.svg)](https://sourcegraph.com/github.com/xanzy/go-gitlab?badge) +[![GoDoc](https://godoc.org/github.com/xanzy/go-gitlab?status.svg)](https://godoc.org/github.com/xanzy/go-gitlab) +[![Go Report Card](https://goreportcard.com/badge/github.com/xanzy/go-gitlab)](https://goreportcard.com/report/github.com/xanzy/go-gitlab) +[![GitHub issues](https://img.shields.io/github/issues/xanzy/go-gitlab.svg)](https://github.com/xanzy/go-gitlab/issues) + +## NOTE + +Release v0.6.0 (released on 25-08-2017) no longer supports the older V3 Gitlab API. If +you need V3 support, please use the `f-api-v3` branch. This release contains some backwards +incompatible changes that were needed to fully support the V4 Gitlab API. ## Coverage -This API client package covers **100%** of the existing GitLab API calls! So this -includes all calls to the following services: +This API client package covers most of the existing Gitlab API calls and is updated regularly +to add new and/or missing endpoints. Currently the following services are supported: -- [x] Users -- [x] Session -- [x] Projects (including setting Webhooks) -- [x] Project Snippets -- [x] Services -- [x] Repositories -- [x] Repository Files -- [x] Commits +- [ ] Discussions (threaded comments) +- [ ] Epic Issues +- [ ] Epics +- [ ] Geo Nodes +- [ ] Project import/export +- [x] Award Emojis - [x] Branches -- [x] Merge Requests -- [x] Issues -- [x] Labels -- [x] Milestones -- [x] Notes (comments) +- [x] Broadcast Messages +- [x] Commits +- [x] Custom Attributes - [x] Deploy Keys -- [x] System Hooks +- [x] Deployments +- [x] Environments +- [x] Events +- [x] Feature flags +- [x] GitLab CI Config templates +- [x] Gitignores templates +- [x] Group Access Requests +- [x] Group Issue Boards +- [x] Group Members +- [x] Group Milestones +- [x] Group-level Variables - [x] Groups +- [x] Issue Boards +- [x] Issues +- [x] Jobs +- [x] Keys +- [x] Labels +- [x] License +- [x] Merge Request Approvals +- [x] Merge Requests - [x] Namespaces +- [x] Notes (comments) +- [x] Notification settings +- [x] Open source license templates +- [x] Pages Domains +- [x] Pipeline Schedules +- [x] Pipeline Triggers +- [x] Pipelines +- [x] Project Access Requests +- [x] Project Clusters +- [x] Project Members +- [x] Project Milestones +- [x] Project Snippets +- [x] Project badges +- [x] Project-level Variables +- [x] Projects (including setting Webhooks) +- [x] Protected Branches +- [x] Protected Tags +- [x] Repositories +- [x] Repository Files +- [x] Runners +- [x] Search +- [x] Services - [x] Settings +- [x] Sidekiq metrics +- [x] System Hooks +- [x] Tags +- [x] Todos +- [x] Users +- [x] Validate CI configuration +- [x] Version +- [x] Wikis ## Usage @@ -42,6 +96,7 @@ users: ```go git := gitlab.NewClient(nil, "yourtokengoeshere") +//git.SetBaseURL("https://git.mydomain.com/api/v3") users, _, err := git.Users.ListUsers() ``` @@ -49,9 +104,9 @@ Some API methods have optional parameters that can be passed. For example, to list all projects for user "svanharmelen": ```go -client := github.NewClient(nil) -opt := &ListProjectsOptions{Search: "svanharmelen"}) -projects, _, err := client.Projects.ListProjects(opt) +git := gitlab.NewClient(nil) +opt := &ListProjectsOptions{Search: gitlab.String("svanharmelen")} +projects, _, err := git.Projects.ListProjects(opt) ``` ### Examples @@ -73,11 +128,11 @@ func main() { // Create new project p := &gitlab.CreateProjectOptions{ - Name: "My Project", - Description: "Just a test project to play with", - MergeRequestsEnabled: true, - SnippetsEnabled: true, - VisibilityLevel: gitlab.PublicVisibility, + Name: gitlab.String("My Project"), + Description: gitlab.String("Just a test project to play with"), + MergeRequestsEnabled: gitlab.Bool(true), + SnippetsEnabled: gitlab.Bool(true), + Visibility: gitlab.Visibility(gitlab.PublicVisibility), } project, _, err := git.Projects.CreateProject(p) if err != nil { @@ -85,18 +140,17 @@ func main() { } // Add a new snippet - s := &gitlab.CreateSnippetOptions{ - Title: "Dummy Snippet", - FileName: "snippet.go", - Code: "package main....", - VisibilityLevel: gitlab.PublicVisibility, + s := &gitlab.CreateProjectSnippetOptions{ + Title: gitlab.String("Dummy Snippet"), + FileName: gitlab.String("snippet.go"), + Code: gitlab.String("package main...."), + Visibility: gitlab.Visibility(gitlab.PublicVisibility), } _, _, err = git.ProjectSnippets.CreateSnippet(project.ID, s) if err != nil { log.Fatal(err) } } - ``` For complete usage of go-gitlab, see the full [package docs](https://godoc.org/github.com/xanzy/go-gitlab). diff --git a/api/access_requests.go b/api/access_requests.go new file mode 100644 index 0000000..609af9d --- /dev/null +++ b/api/access_requests.go @@ -0,0 +1,237 @@ +package gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// AccessRequest represents a access request for a group or project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html +type AccessRequest struct { + ID int `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + RequestedAt *time.Time `json:"requested_at"` + AccessLevel AccessLevelValue `json:"access_level"` +} + +// AccessRequestsService handles communication with the project/group +// access requests related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/access_requests.html +type AccessRequestsService struct { + client *Client +} + +// ListAccessRequestsOptions represents the available +// ListProjectAccessRequests() or ListGroupAccessRequests() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html#list-access-requests-for-a-group-or-project +type ListAccessRequestsOptions ListOptions + +// ListProjectAccessRequests gets a list of access requests +// viewable by the authenticated user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html#list-access-requests-for-a-group-or-project +func (s *AccessRequestsService) ListProjectAccessRequests(pid interface{}, opt *ListAccessRequestsOptions, options ...OptionFunc) ([]*AccessRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/access_requests", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var ars []*AccessRequest + resp, err := s.client.Do(req, &ars) + if err != nil { + return nil, resp, err + } + + return ars, resp, err +} + +// ListGroupAccessRequests gets a list of access requests +// viewable by the authenticated user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html#list-access-requests-for-a-group-or-project +func (s *AccessRequestsService) ListGroupAccessRequests(gid interface{}, opt *ListAccessRequestsOptions, options ...OptionFunc) ([]*AccessRequest, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/access_requests", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var ars []*AccessRequest + resp, err := s.client.Do(req, &ars) + if err != nil { + return nil, resp, err + } + + return ars, resp, err +} + +// RequestProjectAccess requests access for the authenticated user +// to a group or project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html#request-access-to-a-group-or-project +func (s *AccessRequestsService) RequestProjectAccess(pid interface{}, options ...OptionFunc) (*AccessRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/access_requests", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + ar := new(AccessRequest) + resp, err := s.client.Do(req, ar) + if err != nil { + return nil, resp, err + } + + return ar, resp, err +} + +// RequestGroupAccess requests access for the authenticated user +// to a group or project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html#request-access-to-a-group-or-project +func (s *AccessRequestsService) RequestGroupAccess(gid interface{}, options ...OptionFunc) (*AccessRequest, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/access_requests", url.QueryEscape(group)) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + ar := new(AccessRequest) + resp, err := s.client.Do(req, ar) + if err != nil { + return nil, resp, err + } + + return ar, resp, err +} + +// ApproveAccessRequestOptions represents the available +// ApproveProjectAccessRequest() and ApproveGroupAccessRequest() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html#approve-an-access-request +type ApproveAccessRequestOptions struct { + AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"` +} + +// ApproveProjectAccessRequest approves an access request for the given user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html#approve-an-access-request +func (s *AccessRequestsService) ApproveProjectAccessRequest(pid interface{}, user int, opt *ApproveAccessRequestOptions, options ...OptionFunc) (*AccessRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/access_requests/%d/approve", url.QueryEscape(project), user) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ar := new(AccessRequest) + resp, err := s.client.Do(req, ar) + if err != nil { + return nil, resp, err + } + + return ar, resp, err +} + +// ApproveGroupAccessRequest approves an access request for the given user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html#approve-an-access-request +func (s *AccessRequestsService) ApproveGroupAccessRequest(gid interface{}, user int, opt *ApproveAccessRequestOptions, options ...OptionFunc) (*AccessRequest, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/access_requests/%d/approve", url.QueryEscape(group), user) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ar := new(AccessRequest) + resp, err := s.client.Do(req, ar) + if err != nil { + return nil, resp, err + } + + return ar, resp, err +} + +// DenyProjectAccessRequest denies an access request for the given user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html#deny-an-access-request +func (s *AccessRequestsService) DenyProjectAccessRequest(pid interface{}, user int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/access_requests/%d", url.QueryEscape(project), user) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DenyGroupAccessRequest denies an access request for the given user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/access_requests.html#deny-an-access-request +func (s *AccessRequestsService) DenyGroupAccessRequest(gid interface{}, user int, options ...OptionFunc) (*Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("groups/%s/access_requests/%d", url.QueryEscape(group), user) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/award_emojis.go b/api/award_emojis.go new file mode 100644 index 0000000..d335609 --- /dev/null +++ b/api/award_emojis.go @@ -0,0 +1,468 @@ +// +// Copyright 2017, Arkbriar +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// AwardEmojiService handles communication with the emoji awards related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/award_emoji.html +type AwardEmojiService struct { + client *Client +} + +// AwardEmoji represents a GitLab Award Emoji. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/award_emoji.html +type AwardEmoji struct { + ID int `json:"id"` + Name string `json:"name"` + User struct { + Name string `json:"name"` + Username string `json:"username"` + ID int `json:"id"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + } `json:"user"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + AwardableID int `json:"awardable_id"` + AwardableType string `json:"awardable_type"` +} + +const ( + awardMergeRequest = "merge_requests" + awardIssue = "issues" + awardSnippets = "snippets" +) + +// ListAwardEmojiOptions represents the available options for listing emoji +// for each resources +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html +type ListAwardEmojiOptions ListOptions + +// ListMergeRequestAwardEmoji gets a list of all award emoji on the merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#list-an-awardable-39-s-award-emoji +func (s *AwardEmojiService) ListMergeRequestAwardEmoji(pid interface{}, mergeRequestIID int, opt *ListAwardEmojiOptions, options ...OptionFunc) ([]*AwardEmoji, *Response, error) { + return s.listAwardEmoji(pid, awardMergeRequest, mergeRequestIID, opt, options...) +} + +// ListIssueAwardEmoji gets a list of all award emoji on the issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#list-an-awardable-39-s-award-emoji +func (s *AwardEmojiService) ListIssueAwardEmoji(pid interface{}, issueIID int, opt *ListAwardEmojiOptions, options ...OptionFunc) ([]*AwardEmoji, *Response, error) { + return s.listAwardEmoji(pid, awardIssue, issueIID, opt, options...) +} + +// ListSnippetAwardEmoji gets a list of all award emoji on the snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#list-an-awardable-39-s-award-emoji +func (s *AwardEmojiService) ListSnippetAwardEmoji(pid interface{}, snippetID int, opt *ListAwardEmojiOptions, options ...OptionFunc) ([]*AwardEmoji, *Response, error) { + return s.listAwardEmoji(pid, awardSnippets, snippetID, opt, options...) +} + +func (s *AwardEmojiService) listAwardEmoji(pid interface{}, resource string, resourceID int, opt *ListAwardEmojiOptions, options ...OptionFunc) ([]*AwardEmoji, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/award_emoji", + url.QueryEscape(project), + resource, + resourceID, + ) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var as []*AwardEmoji + resp, err := s.client.Do(req, &as) + if err != nil { + return nil, resp, err + } + + return as, resp, err +} + +// GetMergeRequestAwardEmoji get an award emoji from merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#list-an-awardable-39-s-award-emoji +func (s *AwardEmojiService) GetMergeRequestAwardEmoji(pid interface{}, mergeRequestIID, awardID int, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.getAwardEmoji(pid, awardMergeRequest, mergeRequestIID, awardID, options...) +} + +// GetIssueAwardEmoji get an award emoji from issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#list-an-awardable-39-s-award-emoji +func (s *AwardEmojiService) GetIssueAwardEmoji(pid interface{}, issueIID, awardID int, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.getAwardEmoji(pid, awardIssue, issueIID, awardID, options...) +} + +// GetSnippetAwardEmoji get an award emoji from snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#list-an-awardable-39-s-award-emoji +func (s *AwardEmojiService) GetSnippetAwardEmoji(pid interface{}, snippetID, awardID int, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.getAwardEmoji(pid, awardSnippets, snippetID, awardID, options...) +} + +func (s *AwardEmojiService) getAwardEmoji(pid interface{}, resource string, resourceID, awardID int, options ...OptionFunc) (*AwardEmoji, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/award_emoji/%d", + url.QueryEscape(project), + resource, + resourceID, + awardID, + ) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + a := new(AwardEmoji) + resp, err := s.client.Do(req, &a) + if err != nil { + return nil, resp, err + } + + return a, resp, err +} + +// CreateAwardEmojiOptions represents the available options for awarding emoji +// for a resource +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-a-new-emoji +type CreateAwardEmojiOptions struct { + Name string `json:"name"` +} + +// CreateMergeRequestAwardEmoji get an award emoji from merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-a-new-emoji +func (s *AwardEmojiService) CreateMergeRequestAwardEmoji(pid interface{}, mergeRequestIID int, opt *CreateAwardEmojiOptions, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.createAwardEmoji(pid, awardMergeRequest, mergeRequestIID, opt, options...) +} + +// CreateIssueAwardEmoji get an award emoji from issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-a-new-emoji +func (s *AwardEmojiService) CreateIssueAwardEmoji(pid interface{}, issueIID int, opt *CreateAwardEmojiOptions, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.createAwardEmoji(pid, awardIssue, issueIID, opt, options...) +} + +// CreateSnippetAwardEmoji get an award emoji from snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-a-new-emoji +func (s *AwardEmojiService) CreateSnippetAwardEmoji(pid interface{}, snippetID int, opt *CreateAwardEmojiOptions, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.createAwardEmoji(pid, awardSnippets, snippetID, opt, options...) +} + +func (s *AwardEmojiService) createAwardEmoji(pid interface{}, resource string, resourceID int, opt *CreateAwardEmojiOptions, options ...OptionFunc) (*AwardEmoji, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/award_emoji", + url.QueryEscape(project), + resource, + resourceID, + ) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + a := new(AwardEmoji) + resp, err := s.client.Do(req, &a) + if err != nil { + return nil, resp, err + } + + return a, resp, err +} + +// DeleteIssueAwardEmoji delete award emoji on an issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-a-new-emoji-on-a-note +func (s *AwardEmojiService) DeleteIssueAwardEmoji(pid interface{}, issueIID, awardID int, options ...OptionFunc) (*Response, error) { + return s.deleteAwardEmoji(pid, awardMergeRequest, issueIID, awardID, options...) +} + +// DeleteMergeRequestAwardEmoji delete award emoji on a merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-a-new-emoji-on-a-note +func (s *AwardEmojiService) DeleteMergeRequestAwardEmoji(pid interface{}, mergeRequestIID, awardID int, options ...OptionFunc) (*Response, error) { + return s.deleteAwardEmoji(pid, awardMergeRequest, mergeRequestIID, awardID, options...) +} + +// DeleteSnippetAwardEmoji delete award emoji on a snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-a-new-emoji-on-a-note +func (s *AwardEmojiService) DeleteSnippetAwardEmoji(pid interface{}, snippetID, awardID int, options ...OptionFunc) (*Response, error) { + return s.deleteAwardEmoji(pid, awardMergeRequest, snippetID, awardID, options...) +} + +// DeleteAwardEmoji Delete an award emoji on the specified resource. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#delete-an-award-emoji +func (s *AwardEmojiService) deleteAwardEmoji(pid interface{}, resource string, resourceID, awardID int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/award_emoji/%d", url.QueryEscape(project), resource, + resourceID, awardID) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + return s.client.Do(req, nil) +} + +// ListIssuesAwardEmojiOnNote gets a list of all award emoji on a note from the +// issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) ListIssuesAwardEmojiOnNote(pid interface{}, issueID, noteID int, opt *ListAwardEmojiOptions, options ...OptionFunc) ([]*AwardEmoji, *Response, error) { + return s.listAwardEmojiOnNote(pid, awardIssue, issueID, noteID, opt, options...) +} + +// ListMergeRequestAwardEmojiOnNote gets a list of all award emoji on a note +// from the merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) ListMergeRequestAwardEmojiOnNote(pid interface{}, mergeRequestIID, noteID int, opt *ListAwardEmojiOptions, options ...OptionFunc) ([]*AwardEmoji, *Response, error) { + return s.listAwardEmojiOnNote(pid, awardMergeRequest, mergeRequestIID, noteID, opt, options...) +} + +// ListSnippetAwardEmojiOnNote gets a list of all award emoji on a note from the +// snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) ListSnippetAwardEmojiOnNote(pid interface{}, snippetIID, noteID int, opt *ListAwardEmojiOptions, options ...OptionFunc) ([]*AwardEmoji, *Response, error) { + return s.listAwardEmojiOnNote(pid, awardSnippets, snippetIID, noteID, opt, options...) +} + +func (s *AwardEmojiService) listAwardEmojiOnNote(pid interface{}, resources string, ressourceID, noteID int, opt *ListAwardEmojiOptions, options ...OptionFunc) ([]*AwardEmoji, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/notes/%d/award_emoji", url.QueryEscape(project), resources, + ressourceID, noteID) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var as []*AwardEmoji + resp, err := s.client.Do(req, &as) + if err != nil { + return nil, resp, err + } + + return as, resp, err +} + +// GetIssuesAwardEmojiOnNote gets an award emoji on a note from an issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) GetIssuesAwardEmojiOnNote(pid interface{}, issueID, noteID, awardID int, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.getSingleNoteAwardEmoji(pid, awardIssue, issueID, noteID, awardID, options...) +} + +// GetMergeRequestAwardEmojiOnNote gets an award emoji on a note from a +// merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) GetMergeRequestAwardEmojiOnNote(pid interface{}, mergeRequestIID, noteID, awardID int, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.getSingleNoteAwardEmoji(pid, awardMergeRequest, mergeRequestIID, noteID, awardID, + options...) +} + +// GetSnippetAwardEmojiOnNote gets an award emoji on a note from a snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) GetSnippetAwardEmojiOnNote(pid interface{}, snippetIID, noteID, awardID int, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.getSingleNoteAwardEmoji(pid, awardSnippets, snippetIID, noteID, awardID, options...) +} + +func (s *AwardEmojiService) getSingleNoteAwardEmoji(pid interface{}, ressource string, resourceID, noteID, awardID int, options ...OptionFunc) (*AwardEmoji, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/notes/%d/award_emoji/%d", + url.QueryEscape(project), + ressource, + resourceID, + noteID, + awardID, + ) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + a := new(AwardEmoji) + resp, err := s.client.Do(req, &a) + if err != nil { + return nil, resp, err + } + + return a, resp, err +} + +// CreateIssuesAwardEmojiOnNote gets an award emoji on a note from an issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) CreateIssuesAwardEmojiOnNote(pid interface{}, issueID, noteID int, opt *CreateAwardEmojiOptions, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.createAwardEmojiOnNote(pid, awardIssue, issueID, noteID, opt, options...) +} + +// CreateMergeRequestAwardEmojiOnNote gets an award emoji on a note from a +// merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) CreateMergeRequestAwardEmojiOnNote(pid interface{}, mergeRequestIID, noteID int, opt *CreateAwardEmojiOptions, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.createAwardEmojiOnNote(pid, awardMergeRequest, mergeRequestIID, noteID, opt, options...) +} + +// CreateSnippetAwardEmojiOnNote gets an award emoji on a note from a snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) CreateSnippetAwardEmojiOnNote(pid interface{}, snippetIID, noteID int, opt *CreateAwardEmojiOptions, options ...OptionFunc) (*AwardEmoji, *Response, error) { + return s.createAwardEmojiOnNote(pid, awardSnippets, snippetIID, noteID, opt, options...) +} + +// CreateAwardEmojiOnNote award emoji on a note. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-a-new-emoji-on-a-note +func (s *AwardEmojiService) createAwardEmojiOnNote(pid interface{}, resource string, resourceID, noteID int, opt *CreateAwardEmojiOptions, options ...OptionFunc) (*AwardEmoji, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/notes/%d/award_emoji", + url.QueryEscape(project), + resource, + resourceID, + noteID, + ) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + a := new(AwardEmoji) + resp, err := s.client.Do(req, &a) + if err != nil { + return nil, resp, err + } + + return a, resp, err +} + +// DeleteIssuesAwardEmojiOnNote deletes an award emoji on a note from an issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) DeleteIssuesAwardEmojiOnNote(pid interface{}, issueID, noteID, awardID int, options ...OptionFunc) (*Response, error) { + return s.deleteAwardEmojiOnNote(pid, awardIssue, issueID, noteID, awardID, options...) +} + +// DeleteMergeRequestAwardEmojiOnNote deletes an award emoji on a note from a +// merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) DeleteMergeRequestAwardEmojiOnNote(pid interface{}, mergeRequestIID, noteID, awardID int, options ...OptionFunc) (*Response, error) { + return s.deleteAwardEmojiOnNote(pid, awardMergeRequest, mergeRequestIID, noteID, awardID, + options...) +} + +// DeleteSnippetAwardEmojiOnNote deletes an award emoji on a note from a snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/award_emoji.html#award-emoji-on-notes +func (s *AwardEmojiService) DeleteSnippetAwardEmojiOnNote(pid interface{}, snippetIID, noteID, awardID int, options ...OptionFunc) (*Response, error) { + return s.deleteAwardEmojiOnNote(pid, awardSnippets, snippetIID, noteID, awardID, options...) +} + +func (s *AwardEmojiService) deleteAwardEmojiOnNote(pid interface{}, resource string, resourceID, noteID, awardID int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/notes/%d/award_emoji/%d", + url.QueryEscape(project), + resource, + resourceID, + noteID, + awardID, + ) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/boards.go b/api/boards.go new file mode 100644 index 0000000..5d5f83c --- /dev/null +++ b/api/boards.go @@ -0,0 +1,261 @@ +// +// Copyright 2015, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// IssueBoardsService handles communication with the issue board related +// methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html +type IssueBoardsService struct { + client *Client +} + +// IssueBoard represents a GitLab issue board. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html +type IssueBoard struct { + ID int `json:"id"` + Name string `json:"name"` + Project *Project `json:"project"` + Milestone *Milestone `json:"milestone"` + Lists []*BoardList `json:"lists"` +} + +func (b IssueBoard) String() string { + return Stringify(b) +} + +// BoardList represents a GitLab board list. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html +type BoardList struct { + ID int `json:"id"` + Label *Label `json:"label"` + Position int `json:"position"` +} + +func (b BoardList) String() string { + return Stringify(b) +} + +// ListIssueBoardsOptions represents the available ListIssueBoards() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html#project-board +type ListIssueBoardsOptions ListOptions + +// ListIssueBoards gets a list of all issue boards in a project. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html#project-board +func (s *IssueBoardsService) ListIssueBoards(pid interface{}, opt *ListIssueBoardsOptions, options ...OptionFunc) ([]*IssueBoard, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/boards", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var is []*IssueBoard + resp, err := s.client.Do(req, &is) + if err != nil { + return nil, resp, err + } + + return is, resp, err +} + +// GetIssueBoard gets a single issue board of a project. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html#single-board +func (s *IssueBoardsService) GetIssueBoard(pid interface{}, board int, options ...OptionFunc) (*IssueBoard, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/boards/%d", url.QueryEscape(project), board) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ib := new(IssueBoard) + resp, err := s.client.Do(req, ib) + if err != nil { + return nil, resp, err + } + + return ib, resp, err +} + +// GetIssueBoardListsOptions represents the available GetIssueBoardLists() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html#list-board-lists +type GetIssueBoardListsOptions ListOptions + +// GetIssueBoardLists gets a list of the issue board's lists. Does not include +// backlog and closed lists. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html#list-board-lists +func (s *IssueBoardsService) GetIssueBoardLists(pid interface{}, board int, opt *GetIssueBoardListsOptions, options ...OptionFunc) ([]*BoardList, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/boards/%d/lists", url.QueryEscape(project), board) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var bl []*BoardList + resp, err := s.client.Do(req, &bl) + if err != nil { + return nil, resp, err + } + + return bl, resp, err +} + +// GetIssueBoardList gets a single issue board list. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html#single-board-list +func (s *IssueBoardsService) GetIssueBoardList(pid interface{}, board, list int, options ...OptionFunc) (*BoardList, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/boards/%d/lists/%d", + url.QueryEscape(project), + board, + list, + ) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + bl := new(BoardList) + resp, err := s.client.Do(req, bl) + if err != nil { + return nil, resp, err + } + + return bl, resp, err +} + +// CreateIssueBoardListOptions represents the available CreateIssueBoardList() +// options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html#new-board-list +type CreateIssueBoardListOptions struct { + LabelID *int `url:"label_id" json:"label_id"` +} + +// CreateIssueBoardList creates a new issue board list. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html#new-board-list +func (s *IssueBoardsService) CreateIssueBoardList(pid interface{}, board int, opt *CreateIssueBoardListOptions, options ...OptionFunc) (*BoardList, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/boards/%d/lists", url.QueryEscape(project), board) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + bl := new(BoardList) + resp, err := s.client.Do(req, bl) + if err != nil { + return nil, resp, err + } + + return bl, resp, err +} + +// UpdateIssueBoardListOptions represents the available UpdateIssueBoardList() +// options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html#edit-board-list +type UpdateIssueBoardListOptions struct { + Position *int `url:"position" json:"position"` +} + +// UpdateIssueBoardList updates the position of an existing issue board list. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/boards.html#edit-board-list +func (s *IssueBoardsService) UpdateIssueBoardList(pid interface{}, board, list int, opt *UpdateIssueBoardListOptions, options ...OptionFunc) (*BoardList, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/boards/%d/lists/%d", + url.QueryEscape(project), + board, + list, + ) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + bl := new(BoardList) + resp, err := s.client.Do(req, bl) + if err != nil { + return nil, resp, err + } + + return bl, resp, err +} + +// DeleteIssueBoardList soft deletes an issue board list. Only for admins and +// project owners. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/boards.html#delete-a-board-list +func (s *IssueBoardsService) DeleteIssueBoardList(pid interface{}, board, list int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/boards/%d/lists/%d", + url.QueryEscape(project), + board, + list, + ) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/branches.go b/api/branches.go index 9a5ad0a..3f7bd0c 100644 --- a/api/branches.go +++ b/api/branches.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,37 +24,46 @@ import ( // BranchesService handles communication with the branch related methods // of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/branches.html +// GitLab API docs: https://docs.gitlab.com/ce/api/branches.html type BranchesService struct { client *Client } // Branch represents a GitLab branch. // -// GitLab API docs: http://doc.gitlab.com/ce/api/branches.html +// GitLab API docs: https://docs.gitlab.com/ce/api/branches.html type Branch struct { - Commit *Commit `json:"commit"` - Name string `json:"name"` - Protected bool `json:"protected"` + Commit *Commit `json:"commit"` + Name string `json:"name"` + Protected bool `json:"protected"` + Merged bool `json:"merged"` + DevelopersCanPush bool `json:"developers_can_push"` + DevelopersCanMerge bool `json:"developers_can_merge"` } func (b Branch) String() string { return Stringify(b) } +// ListBranchesOptions represents the available ListBranches() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/branches.html#list-repository-branches +type ListBranchesOptions ListOptions + // ListBranches gets a list of repository branches from a project, sorted by // name alphabetically. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/branches.html#list-repository-branches -func (s *BranchesService) ListBranches(pid interface{}) ([]*Branch, *Response, error) { +// https://docs.gitlab.com/ce/api/branches.html#list-repository-branches +func (s *BranchesService) ListBranches(pid interface{}, opts *ListBranchesOptions, options ...OptionFunc) ([]*Branch, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/branches", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, opts, options) if err != nil { return nil, nil, err } @@ -71,15 +80,15 @@ func (s *BranchesService) ListBranches(pid interface{}) ([]*Branch, *Response, e // GetBranch gets a single project repository branch. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/branches.html#get-single-repository-branch -func (s *BranchesService) GetBranch(pid interface{}, branch string) (*Branch, *Response, error) { +// https://docs.gitlab.com/ce/api/branches.html#get-single-repository-branch +func (s *BranchesService) GetBranch(pid interface{}, branch string, options ...OptionFunc) (*Branch, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/repository/branches/%s", url.QueryEscape(project), branch) + u := fmt.Sprintf("projects/%s/repository/branches/%s", url.QueryEscape(project), url.QueryEscape(branch)) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -93,20 +102,29 @@ func (s *BranchesService) GetBranch(pid interface{}, branch string) (*Branch, *R return b, resp, err } +// ProtectBranchOptions represents the available ProtectBranch() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/branches.html#protect-repository-branch +type ProtectBranchOptions struct { + DevelopersCanPush *bool `url:"developers_can_push,omitempty" json:"developers_can_push,omitempty"` + DevelopersCanMerge *bool `url:"developers_can_merge,omitempty" json:"developers_can_merge,omitempty"` +} + // ProtectBranch protects a single project repository branch. This is an // idempotent function, protecting an already protected repository branch // still returns a 200 OK status code. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/branches.html#protect-repository-branch -func (s *BranchesService) ProtectBranch(pid interface{}, branch string) (*Branch, *Response, error) { +// https://docs.gitlab.com/ce/api/branches.html#protect-repository-branch +func (s *BranchesService) ProtectBranch(pid interface{}, branch string, opts *ProtectBranchOptions, options ...OptionFunc) (*Branch, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/repository/branches/%s/protect", url.QueryEscape(project), branch) + u := fmt.Sprintf("projects/%s/repository/branches/%s/protect", url.QueryEscape(project), url.QueryEscape(branch)) - req, err := s.client.NewRequest("PUT", u, nil) + req, err := s.client.NewRequest("PUT", u, opts, options) if err != nil { return nil, nil, err } @@ -125,17 +143,15 @@ func (s *BranchesService) ProtectBranch(pid interface{}, branch string) (*Branch // still returns a 200 OK status code. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/branches.html#unprotect-repository-branch -func (s *BranchesService) UnprotectBranch( - pid interface{}, - branch string) (*Branch, *Response, error) { +// https://docs.gitlab.com/ce/api/branches.html#unprotect-repository-branch +func (s *BranchesService) UnprotectBranch(pid interface{}, branch string, options ...OptionFunc) (*Branch, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/repository/branches/%s/unprotect", url.QueryEscape(project), branch) + u := fmt.Sprintf("projects/%s/repository/branches/%s/unprotect", url.QueryEscape(project), url.QueryEscape(branch)) - req, err := s.client.NewRequest("PUT", u, nil) + req, err := s.client.NewRequest("PUT", u, nil, options) if err != nil { return nil, nil, err } @@ -152,26 +168,24 @@ func (s *BranchesService) UnprotectBranch( // CreateBranchOptions represents the available CreateBranch() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/branches.html#create-repository-branch +// https://docs.gitlab.com/ce/api/branches.html#create-repository-branch type CreateBranchOptions struct { - BranchName string `url:"branch_name,omitempty" json:"branch_name,omitempty"` - Ref string `url:"ref,omitempty" json:"ref,omitempty"` + Branch *string `url:"branch,omitempty" json:"branch,omitempty"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` } // CreateBranch creates branch from commit SHA or existing branch. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/branches.html#create-repository-branch -func (s *BranchesService) CreateBranch( - pid interface{}, - opt *CreateBranchOptions) (*Branch, *Response, error) { +// https://docs.gitlab.com/ce/api/branches.html#create-repository-branch +func (s *BranchesService) CreateBranch(pid interface{}, opt *CreateBranchOptions, options ...OptionFunc) (*Branch, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/branches", url.QueryEscape(project)) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -188,23 +202,37 @@ func (s *BranchesService) CreateBranch( // DeleteBranch deletes an existing branch. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/branches.html#delete-repository-branch -func (s *BranchesService) DeleteBranch(pid interface{}, branch string) (*Response, error) { +// https://docs.gitlab.com/ce/api/branches.html#delete-repository-branch +func (s *BranchesService) DeleteBranch(pid interface{}, branch string, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } - u := fmt.Sprintf("projects/%s/repository/branches/%s", url.QueryEscape(project), branch) + u := fmt.Sprintf("projects/%s/repository/branches/%s", url.QueryEscape(project), url.QueryEscape(branch)) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + return s.client.Do(req, nil) +} + +// DeleteMergedBranches deletes all branches that are merged into the project's default branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/branches.html#delete-merged-branches +func (s *BranchesService) DeleteMergedBranches(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/repository/merged_branches", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { - return resp, err + return nil, err } - return resp, err + return s.client.Do(req, nil) } diff --git a/api/broadcast_messages.go b/api/broadcast_messages.go new file mode 100644 index 0000000..aee852d --- /dev/null +++ b/api/broadcast_messages.go @@ -0,0 +1,172 @@ +// +// Copyright 2018, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "time" +) + +// BroadcastMessagesService handles communication with the broadcast +// messages methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/broadcast_messages.html +type BroadcastMessagesService struct { + client *Client +} + +// BroadcastMessage represents a GitLab issue board. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/broadcast_messages.html#get-all-broadcast-messages +type BroadcastMessage struct { + Message string `json:"message"` + StartsAt *time.Time `json:"starts_at"` + EndsAt *time.Time `json:"ends_at"` + Color string `json:"color"` + Font string `json:"font"` + ID int `json:"id"` + Active bool `json:"active"` +} + +// ListBroadcastMessagesOptions represents the available ListBroadcastMessages() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/broadcast_messages.html#get-all-broadcast-messages +type ListBroadcastMessagesOptions ListOptions + +// ListBroadcastMessages gets a list of all broadcasted messages. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/broadcast_messages.html#get-all-broadcast-messages +func (s *BroadcastMessagesService) ListBroadcastMessages(opt *ListBroadcastMessagesOptions, options ...OptionFunc) ([]*BroadcastMessage, *Response, error) { + req, err := s.client.NewRequest("GET", "broadcast_messages", opt, options) + if err != nil { + return nil, nil, err + } + + var bs []*BroadcastMessage + resp, err := s.client.Do(req, &bs) + if err != nil { + return nil, resp, err + } + + return bs, resp, err +} + +// GetBroadcastMessage gets a single broadcast message. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/broadcast_messages.html#get-a-specific-broadcast-message +func (s *BroadcastMessagesService) GetBroadcastMessage(broadcast int, options ...OptionFunc) (*BroadcastMessage, *Response, error) { + u := fmt.Sprintf("broadcast_messages/%d", broadcast) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + b := new(BroadcastMessage) + resp, err := s.client.Do(req, &b) + if err != nil { + return nil, resp, err + } + + return b, resp, err +} + +// CreateBroadcastMessageOptions represents the available CreateBroadcastMessage() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/broadcast_messages.html#create-a-broadcast-message +type CreateBroadcastMessageOptions struct { + Message *string `url:"message" json:"message"` + StartsAt *time.Time `url:"starts_at,omitempty" json:"starts_at,omitempty"` + EndsAt *time.Time `url:"ends_at,omitempty" json:"ends_at,omitempty"` + Color *string `url:"color,omitempty" json:"color,omitempty"` + Font *string `url:"font,omitempty" json:"font,omitempty"` +} + +// CreateBroadcastMessage creates a message to broadcast. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/broadcast_messages.html#create-a-broadcast-message +func (s *BroadcastMessagesService) CreateBroadcastMessage(opt *CreateBroadcastMessageOptions, options ...OptionFunc) (*BroadcastMessage, *Response, error) { + req, err := s.client.NewRequest("POST", "broadcast_messages", opt, options) + if err != nil { + return nil, nil, err + } + + b := new(BroadcastMessage) + resp, err := s.client.Do(req, &b) + if err != nil { + return nil, resp, err + } + + return b, resp, err +} + +// UpdateBroadcastMessageOptions represents the available CreateBroadcastMessage() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/broadcast_messages.html#update-a-broadcast-message +type UpdateBroadcastMessageOptions struct { + Message *string `url:"message,omitempty" json:"message,omitempty"` + StartsAt *time.Time `url:"starts_at,omitempty" json:"starts_at,omitempty"` + EndsAt *time.Time `url:"ends_at,omitempty" json:"ends_at,omitempty"` + Color *string `url:"color,omitempty" json:"color,omitempty"` + Font *string `url:"font,omitempty" json:"font,omitempty"` +} + +// UpdateBroadcastMessage update a broadcasted message. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/broadcast_messages.html#update-a-broadcast-message +func (s *BroadcastMessagesService) UpdateBroadcastMessage(broadcast int, opt *UpdateBroadcastMessageOptions, options ...OptionFunc) (*BroadcastMessage, *Response, error) { + u := fmt.Sprintf("broadcast_messages/%d", broadcast) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + b := new(BroadcastMessage) + resp, err := s.client.Do(req, &b) + if err != nil { + return nil, resp, err + } + + return b, resp, err +} + +// DeleteBroadcastMessage deletes a broadcasted message. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/broadcast_messages.html#delete-a-broadcast-message +func (s *BroadcastMessagesService) DeleteBroadcastMessage(broadcast int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("broadcast_messages/%d", broadcast) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/broadcast_messages_test.go b/api/broadcast_messages_test.go new file mode 100644 index 0000000..5d18c40 --- /dev/null +++ b/api/broadcast_messages_test.go @@ -0,0 +1,217 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" + "time" +) + +func TestListBroadcastMessages(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/broadcast_messages", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprintf(w, `[{ + "message": "Some Message", + "starts_at": "2017-06-26T06:00:00.000Z", + "ends_at": "2017-06-27T12:59:00.000Z", + "color": "#E75E40", + "font": "#FFFFFF", + "id": 1, + "active": false + },{ + "message": "SomeMessage2", + "starts_at": "2015-04-27T06:43:00.000Z", + "ends_at": "2015-04-28T20:43:00.000Z", + "color": "#AA33EE", + "font": "#224466", + "id": 2, + "active": true + }]`) + }) + + got, _, err := client.BroadcastMessage.ListBroadcastMessages(nil, nil) + if err != nil { + t.Errorf("ListBroadcastMessages returned error: %v", err) + } + + wantedFirstStartsAt := time.Date(2017, 06, 26, 6, 0, 0, 0, time.UTC) + wantedFirstEndsAt := time.Date(2017, 06, 27, 12, 59, 0, 0, time.UTC) + + wantedSecondStartsAt := time.Date(2015, 04, 27, 6, 43, 0, 0, time.UTC) + wantedSecondEndsAt := time.Date(2015, 04, 28, 20, 43, 0, 0, time.UTC) + + want := []*BroadcastMessage{{ + Message: "Some Message", + StartsAt: &wantedFirstStartsAt, + EndsAt: &wantedFirstEndsAt, + Color: "#E75E40", + Font: "#FFFFFF", + ID: 1, + Active: false, + }, { + Message: "SomeMessage2", + StartsAt: &wantedSecondStartsAt, + EndsAt: &wantedSecondEndsAt, + Color: "#AA33EE", + Font: "#224466", + ID: 2, + Active: true, + }} + + if !reflect.DeepEqual(got, want) { + t.Errorf("ListBroadcastMessages returned \ngot:\n%v\nwant:\n%v", Stringify(got), Stringify(want)) + } +} + +func TestGetBroadcastMessages(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/broadcast_messages/1/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprintf(w, `{ + "message": "Some Message", + "starts_at": "2017-06-26T06:00:00.000Z", + "ends_at": "2017-06-27T12:59:00.000Z", + "color": "#E75E40", + "font": "#FFFFFF", + "id": 1, + "active": false + }`) + }) + + got, _, err := client.BroadcastMessage.GetBroadcastMessage(1) + if err != nil { + t.Errorf("GetBroadcastMessage returned error: %v", err) + } + + wantedStartsAt := time.Date(2017, time.June, 26, 6, 0, 0, 0, time.UTC) + wantedEndsAt := time.Date(2017, time.June, 27, 12, 59, 0, 0, time.UTC) + + want := &BroadcastMessage{ + Message: "Some Message", + StartsAt: &wantedStartsAt, + EndsAt: &wantedEndsAt, + Color: "#E75E40", + Font: "#FFFFFF", + ID: 1, + Active: false, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("GetBroadcastMessage returned \ngot:\n%v\nwant:\n%v", Stringify(got), Stringify(want)) + } +} + +func TestCreateBroadcastMessages(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + wantedStartsAt := time.Date(2017, time.June, 26, 6, 0, 0, 0, time.UTC) + wantedEndsAt := time.Date(2017, time.June, 27, 12, 59, 0, 0, time.UTC) + + mux.HandleFunc("/api/v4/broadcast_messages", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprintf(w, `{ + "message": "Some Message", + "starts_at": "2017-06-26T06:00:00.000Z", + "ends_at": "2017-06-27T12:59:00.000Z", + "color": "#E75E40", + "font": "#FFFFFF", + "id": 42, + "active": false + }`) + }) + + opt := &CreateBroadcastMessageOptions{ + Message: String("Some Message"), + StartsAt: &wantedStartsAt, + EndsAt: &wantedEndsAt, + Color: String("#E75E40"), + Font: String("#FFFFFF"), + } + + got, _, err := client.BroadcastMessage.CreateBroadcastMessage(opt) + if err != nil { + t.Errorf("CreateBroadcastMessage returned error: %v", err) + } + + want := &BroadcastMessage{ + Message: "Some Message", + StartsAt: &wantedStartsAt, + EndsAt: &wantedEndsAt, + Color: "#E75E40", + Font: "#FFFFFF", + ID: 42, + Active: false, + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("CreateBroadcastMessage returned \ngot:\n%v\nwant:\n%v", Stringify(got), Stringify(want)) + } +} + +func TestUpdateBroadcastMessages(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + wantedStartsAt := time.Date(2017, time.June, 26, 6, 0, 0, 0, time.UTC) + wantedEndsAt := time.Date(2017, time.June, 27, 12, 59, 0, 0, time.UTC) + + mux.HandleFunc("/api/v4/broadcast_messages/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprintf(w, `{ + "message": "Some Message Updated", + "starts_at": "2017-06-26T06:00:00.000Z", + "ends_at": "2017-06-27T12:59:00.000Z", + "color": "#E75E40", + "font": "#FFFFFF", + "id": 42, + "active": false + }`) + }) + + opt := &UpdateBroadcastMessageOptions{ + Message: String("Some Message Updated"), + StartsAt: &wantedStartsAt, + EndsAt: &wantedEndsAt, + Color: String("#E75E40"), + Font: String("#FFFFFF"), + } + + got, _, err := client.BroadcastMessage.UpdateBroadcastMessage(1, opt) + if err != nil { + t.Errorf("UpdateBroadcastMessage returned error: %v", err) + } + + want := &BroadcastMessage{ + Message: "Some Message Updated", + StartsAt: &wantedStartsAt, + EndsAt: &wantedEndsAt, + Color: "#E75E40", + Font: "#FFFFFF", + ID: 42, + Active: false, + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("UpdateBroadcastMessage returned \ngot:\n%v\nwant:\n%v", Stringify(got), Stringify(want)) + } +} + +func TestDeleteBroadcastMessages(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/broadcast_messages/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.BroadcastMessage.DeleteBroadcastMessage(1) + if err != nil { + t.Errorf("UpdateBroadcastMessage returned error: %v", err) + } +} diff --git a/api/build_variables.go b/api/build_variables.go new file mode 100644 index 0000000..8a6c8cd --- /dev/null +++ b/api/build_variables.go @@ -0,0 +1,173 @@ +package gitlab + +import ( + "fmt" + "net/url" +) + +// BuildVariablesService handles communication with the project variables related methods +// of the Gitlab API +// +// Gitlab API Docs : https://docs.gitlab.com/ce/api/build_variables.html +type BuildVariablesService struct { + client *Client +} + +// BuildVariable represents a variable available for each build of the given project +// +// Gitlab API Docs : https://docs.gitlab.com/ce/api/build_variables.html +type BuildVariable struct { + Key string `json:"key"` + Value string `json:"value"` + Protected bool `json:"protected"` +} + +func (v BuildVariable) String() string { + return Stringify(v) +} + +// ListBuildVariablesOptions are the parameters to ListBuildVariables() +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#list-project-variables +type ListBuildVariablesOptions ListOptions + +// ListBuildVariables gets the a list of project variables in a project +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#list-project-variables +func (s *BuildVariablesService) ListBuildVariables(pid interface{}, opts *ListBuildVariablesOptions, options ...OptionFunc) ([]*BuildVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opts, options) + if err != nil { + return nil, nil, err + } + + var v []*BuildVariable + resp, err := s.client.Do(req, &v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// GetBuildVariable gets a single project variable of a project +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#show-variable-details +func (s *BuildVariablesService) GetBuildVariable(pid interface{}, key string, options ...OptionFunc) (*BuildVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables/%s", url.QueryEscape(project), key) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + v := new(BuildVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// CreateBuildVariableOptions are the parameters to CreateBuildVariable() +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#create-variable +type CreateBuildVariableOptions struct { + Key *string `url:"key" json:"key"` + Value *string `url:"value" json:"value"` + Protected *bool `url:"protected,omitempty" json:"protected,omitempty"` +} + +// CreateBuildVariable creates a variable for a given project +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#create-variable +func (s *BuildVariablesService) CreateBuildVariable(pid interface{}, opt *CreateBuildVariableOptions, options ...OptionFunc) (*BuildVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + v := new(BuildVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// UpdateBuildVariableOptions are the parameters to UpdateBuildVariable() +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#update-variable +type UpdateBuildVariableOptions struct { + Key *string `url:"key" json:"key"` + Value *string `url:"value" json:"value"` + Protected *bool `url:"protected,omitempty" json:"protected,omitempty"` +} + +// UpdateBuildVariable updates an existing project variable +// The variable key must exist +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#update-variable +func (s *BuildVariablesService) UpdateBuildVariable(pid interface{}, key string, opt *UpdateBuildVariableOptions, options ...OptionFunc) (*BuildVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables/%s", url.QueryEscape(project), key) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + v := new(BuildVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// RemoveBuildVariable removes a project variable of a given project identified by its key +// +// Gitlab API Docs: +// https://docs.gitlab.com/ce/api/build_variables.html#remove-variable +func (s *BuildVariablesService) RemoveBuildVariable(pid interface{}, key string, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/variables/%s", url.QueryEscape(project), key) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/build_variables_test.go b/api/build_variables_test.go new file mode 100644 index 0000000..877fc20 --- /dev/null +++ b/api/build_variables_test.go @@ -0,0 +1,113 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +const ( + myKey = "MY_KEY" + myValue = "MY_VALUE" + myKey2 = "MY_KEY2" + myValue2 = "MY_VALUE2" + myNewValue = "MY_NEW_VALUE" +) + +func TestListBuildVariables(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/variables", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprintf(w, + `[{"key":"%s","value":"%s"},{"key":"%s","value":"%s"}]`, myKey, myValue, myKey2, myValue2) + }) + + variables, _, err := client.BuildVariables.ListBuildVariables(1, nil) + if err != nil { + t.Errorf("ListBuildVariables returned error: %v", err) + } + + want := []*BuildVariable{{Key: myKey, Value: myValue}, {Key: myKey2, Value: myValue2}} + if !reflect.DeepEqual(want, variables) { + t.Errorf("ListBuildVariables returned %+v, want %+v", variables, want) + } +} + +func TestGetBuildVariable(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/variables/"+myKey, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprintf(w, `{"key":"%s","value":"%s"}`, myKey, myValue) + }) + + variable, _, err := client.BuildVariables.GetBuildVariable(1, myKey) + if err != nil { + t.Errorf("GetBuildVariable returned error: %v", err) + } + + want := &BuildVariable{Key: myKey, Value: myValue} + if !reflect.DeepEqual(want, variable) { + t.Errorf("GetBuildVariable returned %+v, want %+v", variable, want) + } +} + +func TestCreateBuildVariable(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/variables", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprintf(w, `{"key":"%s","value":"%s", "protected": false}`, myKey, myValue) + }) + + opt := &CreateBuildVariableOptions{String(myKey), String(myValue), Bool(false)} + variable, _, err := client.BuildVariables.CreateBuildVariable(1, opt) + if err != nil { + t.Errorf("CreateBuildVariable returned error: %v", err) + } + + want := &BuildVariable{Key: myKey, Value: myValue, Protected: false} + if !reflect.DeepEqual(want, variable) { + t.Errorf("CreateBuildVariable returned %+v, want %+v", variable, want) + } +} + +func TestUpdateBuildVariable(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/variables/"+myKey, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprintf(w, `{"key":"%s","value":"%s", "protected": false}`, myKey, myNewValue) + }) + + opt := &UpdateBuildVariableOptions{String(myKey), String(myNewValue), Bool(false)} + variable, _, err := client.BuildVariables.UpdateBuildVariable(1, myKey, opt) + if err != nil { + t.Errorf("UpdateBuildVariable returned error: %v", err) + } + + want := &BuildVariable{Key: myKey, Value: myNewValue, Protected: false} + if !reflect.DeepEqual(want, variable) { + t.Errorf("UpdateBuildVariable returned %+v, want %+v", variable, want) + } +} + +func TestRemoveBuildVariable(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/variables/"+myKey, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.BuildVariables.RemoveBuildVariable(1, myKey) + if err != nil { + t.Errorf("RemoveBuildVariable returned error: %v", err) + } +} diff --git a/api/ci_yml_templates.go b/api/ci_yml_templates.go new file mode 100644 index 0000000..abd8566 --- /dev/null +++ b/api/ci_yml_templates.go @@ -0,0 +1,70 @@ +package gitlab + +import ( + "fmt" + "net/url" +) + +// CIYMLTemplatesService handles communication with the gitlab +// CI YML templates related methods of the GitLab API. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html +type CIYMLTemplatesService struct { + client *Client +} + +// CIYMLTemplate represents a GitLab CI YML template. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html +type CIYMLTemplate struct { + Name string `json:"name"` + Content string `json:"content"` +} + +// ListCIYMLTemplatesOptions represents the available ListAllTemplates() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/gitignores.html#list-gitignore-templates +type ListCIYMLTemplatesOptions ListOptions + +// ListAllTemplates get all GitLab CI YML templates. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html#list-gitlab-ci-yml-templates +func (s *CIYMLTemplatesService) ListAllTemplates(opt *ListCIYMLTemplatesOptions, options ...OptionFunc) ([]*CIYMLTemplate, *Response, error) { + req, err := s.client.NewRequest("GET", "templates/gitlab_ci_ymls", opt, options) + if err != nil { + return nil, nil, err + } + + var cts []*CIYMLTemplate + resp, err := s.client.Do(req, &cts) + if err != nil { + return nil, resp, err + } + + return cts, resp, err +} + +// GetTemplate get a single GitLab CI YML template. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html#single-gitlab-ci-yml-template +func (s *CIYMLTemplatesService) GetTemplate(key string, options ...OptionFunc) (*CIYMLTemplate, *Response, error) { + u := fmt.Sprintf("templates/gitlab_ci_ymls/%s", url.QueryEscape(key)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ct := new(CIYMLTemplate) + resp, err := s.client.Do(req, ct) + if err != nil { + return nil, resp, err + } + + return ct, resp, err +} diff --git a/api/commits.go b/api/commits.go index 03f93a9..1bbcc40 100644 --- a/api/commits.go +++ b/api/commits.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,25 +25,38 @@ import ( // CommitsService handles communication with the commit related methods // of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html type CommitsService struct { client *Client } // Commit represents a GitLab commit. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html type Commit struct { - ID string `json:"id"` - ShortID string `json:"short_id"` - Title string `json:"title"` - AuthorName string `json:"author_name"` - AuthorEmail string `json:"author_email"` - AuthoredDate time.Time `json:"authored_date"` - CommittedDate time.Time `json:"committed_date"` - CreatedAt time.Time `json:"created_at"` - Message string `json:"message"` - ParentsIds []string `json:"parents_ids"` + ID string `json:"id"` + ShortID string `json:"short_id"` + Title string `json:"title"` + AuthorName string `json:"author_name"` + AuthorEmail string `json:"author_email"` + AuthoredDate *time.Time `json:"authored_date"` + CommitterName string `json:"committer_name"` + CommitterEmail string `json:"committer_email"` + CommittedDate *time.Time `json:"committed_date"` + CreatedAt *time.Time `json:"created_at"` + Message string `json:"message"` + ParentIDs []string `json:"parent_ids"` + Stats *CommitStats `json:"stats"` + Status *BuildStateValue `json:"status"` +} + +// CommitStats represents the number of added and deleted files in a commit. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html +type CommitStats struct { + Additions int `json:"additions"` + Deletions int `json:"deletions"` + Total int `json:"total"` } func (c Commit) String() string { @@ -52,25 +65,28 @@ func (c Commit) String() string { // ListCommitsOptions represents the available ListCommits() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html#list-commits +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#list-repository-commits type ListCommitsOptions struct { ListOptions - RefName string `url:"ref_name,omitempty" json:"ref_name,omitempty"` + RefName *string `url:"ref_name,omitempty" json:"ref_name,omitempty"` + Since *time.Time `url:"since,omitempty" json:"since,omitempty"` + Until *time.Time `url:"until,omitempty" json:"until,omitempty"` + Path *string `url:"path,omitempty" json:"path,omitempty"` + All *bool `url:"all,omitempty" json:"all,omitempty"` + WithStats *bool `url:"with_stats,omitempty" json:"with_stats,omitempty"` } // ListCommits gets a list of repository commits in a project. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html#list-commits -func (s *CommitsService) ListCommits( - pid interface{}, - opt *ListCommitsOptions) ([]*Commit, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#list-commits +func (s *CommitsService) ListCommits(pid interface{}, opt *ListCommitsOptions, options ...OptionFunc) ([]*Commit, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/commits", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -84,20 +100,83 @@ func (s *CommitsService) ListCommits( return c, resp, err } +// FileAction represents the available actions that can be performed on a file. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions +type FileAction string + +// The available file actions. +const ( + FileCreate FileAction = "create" + FileDelete FileAction = "delete" + FileMove FileAction = "move" + FileUpdate FileAction = "update" +) + +// CommitAction represents a single file action within a commit. +type CommitAction struct { + Action FileAction `url:"action" json:"action"` + FilePath string `url:"file_path" json:"file_path"` + PreviousPath string `url:"previous_path,omitempty" json:"previous_path,omitempty"` + Content string `url:"content,omitempty" json:"content,omitempty"` + Encoding string `url:"encoding,omitempty" json:"encoding,omitempty"` +} + +// CommitRef represents the reference of branches/tags in a commit. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/commits.html#get-references-a-commit-is-pushed-to +type CommitRef struct { + Type string `json:"type"` + Name string `json:"name"` +} + +// GetCommitRefsOptions represents the available GetCommitRefs() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/commits.html#get-references-a-commit-is-pushed-to +type GetCommitRefsOptions struct { + ListOptions + Type *string `url:"type,omitempty" json:"type,omitempty"` +} + +// GetCommitRefs gets all references (from branches or tags) a commit is pushed to +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/commits.html#get-references-a-commit-is-pushed-to +func (s *CommitsService) GetCommitRefs(pid interface{}, sha string, opt *GetCommitRefsOptions, options ...OptionFunc) ([]CommitRef, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/refs", url.QueryEscape(project), sha) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var cs []CommitRef + resp, err := s.client.Do(req, &cs) + if err != nil { + return nil, resp, err + } + + return cs, resp, err +} + // GetCommit gets a specific commit identified by the commit hash or name of a // branch or tag. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html#get-a-single-commit -func (s *CommitsService) GetCommit( - pid interface{}, - sha string) (*Commit, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-a-single-commit +func (s *CommitsService) GetCommit(pid interface{}, sha string, options ...OptionFunc) (*Commit, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/commits/%s", url.QueryEscape(project), sha) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -111,9 +190,45 @@ func (s *CommitsService) GetCommit( return c, resp, err } +// CreateCommitOptions represents the available options for a new commit. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions +type CreateCommitOptions struct { + Branch *string `url:"branch" json:"branch"` + CommitMessage *string `url:"commit_message" json:"commit_message"` + StartBranch *string `url:"start_branch,omitempty" json:"start_branch,omitempty"` + Actions []*CommitAction `url:"actions" json:"actions"` + AuthorEmail *string `url:"author_email,omitempty" json:"author_email,omitempty"` + AuthorName *string `url:"author_name,omitempty" json:"author_name,omitempty"` +} + +// CreateCommit creates a commit with multiple files and actions. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions +func (s *CommitsService) CreateCommit(pid interface{}, opt *CreateCommitOptions, options ...OptionFunc) (*Commit, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + var c *Commit + resp, err := s.client.Do(req, &c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} + // Diff represents a GitLab diff. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html type Diff struct { Diff string `json:"diff"` NewPath string `json:"new_path"` @@ -129,20 +244,24 @@ func (d Diff) String() string { return Stringify(d) } +// GetCommitDiffOptions represents the available GetCommitDiff() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/commits.html#get-the-diff-of-a-commit +type GetCommitDiffOptions ListOptions + // GetCommitDiff gets the diff of a commit in a project.. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/commits.html#get-the-diff-of-a-commit -func (s *CommitsService) GetCommitDiff( - pid interface{}, - sha string) ([]*Diff, *Response, error) { +// https://docs.gitlab.com/ce/api/commits.html#get-the-diff-of-a-commit +func (s *CommitsService) GetCommitDiff(pid interface{}, sha string, opt *GetCommitDiffOptions, options ...OptionFunc) ([]*Diff, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/commits/%s/diff", url.QueryEscape(project), sha) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -158,7 +277,7 @@ func (s *CommitsService) GetCommitDiff( // CommitComment represents a GitLab commit comment. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html type CommitComment struct { Note string `json:"note"` Path string `json:"path"` @@ -167,34 +286,39 @@ type CommitComment struct { Author Author `json:"author"` } +// Author represents a GitLab commit author type Author struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - State string `json:"state"` - Blocked bool `json:"blocked"` - CreatedAt time.Time `json:"created_at"` + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + Blocked bool `json:"blocked"` + CreatedAt *time.Time `json:"created_at"` } func (c CommitComment) String() string { return Stringify(c) } +// GetCommitCommentsOptions represents the available GetCommitComments() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/commits.html#get-the-comments-of-a-commit +type GetCommitCommentsOptions ListOptions + // GetCommitComments gets the comments of a commit in a project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/commits.html#get-the-comments-of-a-commit -func (s *CommitsService) GetCommitComments( - pid interface{}, - sha string) ([]*CommitComment, *Response, error) { +// https://docs.gitlab.com/ce/api/commits.html#get-the-comments-of-a-commit +func (s *CommitsService) GetCommitComments(pid interface{}, sha string, opt *GetCommitCommentsOptions, options ...OptionFunc) ([]*CommitComment, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/commits/%s/comments", url.QueryEscape(project), sha) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -212,12 +336,12 @@ func (s *CommitsService) GetCommitComments( // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/commits.html#post-comment-to-commit +// https://docs.gitlab.com/ce/api/commits.html#post-comment-to-commit type PostCommitCommentOptions struct { - Note string `url:"note,omitempty" json:"note,omitempty"` - Path string `url:"path" json:"path"` - Line int `url:"line" json:"line"` - LineType string `url:"line_type" json:"line_type"` + Note *string `url:"note,omitempty" json:"note,omitempty"` + Path *string `url:"path" json:"path"` + Line *int `url:"line" json:"line"` + LineType *string `url:"line_type" json:"line_type"` } // PostCommitComment adds a comment to a commit. Optionally you can post @@ -225,18 +349,15 @@ type PostCommitCommentOptions struct { // line_old are required. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/commits.html#post-comment-to-commit -func (s *CommitsService) PostCommitComment( - pid interface{}, - sha string, - opt *PostCommitCommentOptions) (*CommitComment, *Response, error) { +// https://docs.gitlab.com/ce/api/commits.html#post-comment-to-commit +func (s *CommitsService) PostCommitComment(pid interface{}, sha string, opt *PostCommitCommentOptions, options ...OptionFunc) (*CommitComment, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/commits/%s/comments", url.QueryEscape(project), sha) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -252,45 +373,43 @@ func (s *CommitsService) PostCommitComment( // GetCommitStatusesOptions represents the available GetCommitStatuses() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit type GetCommitStatusesOptions struct { - Ref string `url:"ref,omitempty" json:"ref,omitempty"` - Stage string `url:"stage,omitempty" json:"stage,omitempty"` - Name string `url:"name,omitempty" json:"name,omitempty"` - All bool `url:"all,omitempty" json:"all,omitempty"` + ListOptions + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` + Stage *string `url:"stage,omitempty" json:"stage,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + All *bool `url:"all,omitempty" json:"all,omitempty"` } // CommitStatus represents a GitLab commit status. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit type CommitStatus struct { - ID int `json:"id"` - SHA string `json:"sha"` - Ref string `json:"ref"` - Status string `json:"status"` - Name string `json:"name"` - TargetUrl string `json:"target_url"` - Description string `json:"description"` - CreatedAt time.Time `json:"created_at"` - StartedAt time.Time `json:"started_at"` - FinishedAt time.Time `json:"finished_at"` - Author Author `json:"author"` + ID int `json:"id"` + SHA string `json:"sha"` + Ref string `json:"ref"` + Status string `json:"status"` + Name string `json:"name"` + TargetURL string `json:"target_url"` + Description string `json:"description"` + CreatedAt *time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at"` + FinishedAt *time.Time `json:"finished_at"` + Author Author `json:"author"` } // GetCommitStatuses gets the statuses of a commit in a project. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit -func (s *CommitsService) GetCommitStatuses( - pid interface{}, - sha string, - opt *GetCommitStatusesOptions) ([]*CommitStatus, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#get-the-status-of-a-commit +func (s *CommitsService) GetCommitStatuses(pid interface{}, sha string, opt *GetCommitStatusesOptions, options ...OptionFunc) ([]*CommitStatus, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/commits/%s/statuses", url.QueryEscape(project), sha) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -306,40 +425,27 @@ func (s *CommitsService) GetCommitStatuses( // SetCommitStatusOptions represents the available SetCommitStatus() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html#post-the-status-to-commit +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#post-the-status-to-commit type SetCommitStatusOptions struct { - State BuildState `url:"state" json:"state"` - Ref string `url:"ref,omitempty" json:"ref,omitempty"` - Name string `url:"name,omitempty" json:"name,omitempty"` - Context string `url:"context,omitempty" json:"context,omitempty"` - TargetUrl string `url:"target_url,omitempty" json:"target_url,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` + State BuildStateValue `url:"state" json:"state"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + Context *string `url:"context,omitempty" json:"context,omitempty"` + TargetURL *string `url:"target_url,omitempty" json:"target_url,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` } -type BuildState string - -const ( - Pending BuildState = "pending" - Running BuildState = "running" - Success BuildState = "success" - Failed BuildState = "failed" - Canceled BuildState = "canceled" -) - // SetCommitStatus sets the status of a commit in a project. // -// GitLab API docs: http://doc.gitlab.com/ce/api/commits.html#post-the-status-to-commit -func (s *CommitsService) SetCommitStatus( - pid interface{}, - sha string, - opt *SetCommitStatusOptions) (*CommitStatus, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#post-the-status-to-commit +func (s *CommitsService) SetCommitStatus(pid interface{}, sha string, opt *SetCommitStatusOptions, options ...OptionFunc) (*CommitStatus, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/statuses/%s", url.QueryEscape(project), sha) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -352,3 +458,61 @@ func (s *CommitsService) SetCommitStatus( return cs, resp, err } + +// GetMergeRequestsByCommit gets merge request associated with a commit. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/commits.html#list-merge-requests-associated-with-a-commit +func (s *CommitsService) GetMergeRequestsByCommit(pid interface{}, sha string, options ...OptionFunc) ([]*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/merge_requests", + url.QueryEscape(project), url.QueryEscape(sha)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var mrs []*MergeRequest + resp, err := s.client.Do(req, &mrs) + if err != nil { + return nil, resp, err + } + + return mrs, resp, err +} + +// CherryPickCommitOptions represents the available options for cherry-picking a commit. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#cherry-pick-a-commit +type CherryPickCommitOptions struct { + TargetBranch *string `url:"branch" json:"branch,omitempty"` +} + +// CherryPickCommit sherry picks a commit to a given branch. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/commits.html#cherry-pick-a-commit +func (s *CommitsService) CherryPickCommit(pid interface{}, sha string, opt *CherryPickCommitOptions, options ...OptionFunc) (*Commit, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/cherry_pick", + url.QueryEscape(project), url.QueryEscape(sha)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + var c *Commit + resp, err := s.client.Do(req, &c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} diff --git a/api/commits_test.go b/api/commits_test.go index 709c97c..81eeb29 100644 --- a/api/commits_test.go +++ b/api/commits_test.go @@ -11,18 +11,12 @@ func TestGetCommitStatuses(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects/1/repository/commits/b0b3a907f41409829b307a28b82fdbd552ee5a27/statuses", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects/1/repository/commits/b0b3a907f41409829b307a28b82fdbd552ee5a27/statuses", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - testFormValues(t, r, values{ - "ref": "master", - "stage": "test", - "name": "ci/jenkins", - "all": "true", - }) fmt.Fprint(w, `[{"id":1}]`) }) - opt := &GetCommitStatusesOptions{"master", "test", "ci/jenkins", true} + opt := &GetCommitStatusesOptions{Ref: String("master"), Stage: String("test"), Name: String("ci/jenkins"), All: Bool(true)} statuses, _, err := client.Commits.GetCommitStatuses("1", "b0b3a907f41409829b307a28b82fdbd552ee5a27", opt) if err != nil { @@ -39,19 +33,12 @@ func TestSetCommitStatus(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects/1/statuses/b0b3a907f41409829b307a28b82fdbd552ee5a27", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects/1/statuses/b0b3a907f41409829b307a28b82fdbd552ee5a27", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "POST") - testJsonBody(t, r, values{ - "state": "running", - "ref": "master", - "name": "ci/jenkins", - "target_url": "http://abc", - "description": "build", - }) fmt.Fprint(w, `{"id":1}`) }) - opt := &SetCommitStatusOptions{Running, "master", "ci/jenkins", "", "http://abc", "build"} + opt := &SetCommitStatusOptions{State: Running, Ref: String("master"), Name: String("ci/jenkins"), Context: String(""), TargetURL: String("http://abc"), Description: String("build")} status, _, err := client.Commits.SetCommitStatus("1", "b0b3a907f41409829b307a28b82fdbd552ee5a27", opt) if err != nil { diff --git a/api/custom_attributes.go b/api/custom_attributes.go new file mode 100644 index 0000000..ce165c8 --- /dev/null +++ b/api/custom_attributes.go @@ -0,0 +1,171 @@ +package gitlab + +import ( + "fmt" +) + +// CustomAttributesService handles communication with the group, project and +// user custom attributes related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/custom_attributes.html +type CustomAttributesService struct { + client *Client +} + +// CustomAttribute struct is used to unmarshal response to api calls. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/custom_attributes.html +type CustomAttribute struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// ListCustomUserAttributes lists the custom attributes of the specified user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#list-custom-attributes +func (s *CustomAttributesService) ListCustomUserAttributes(user int, options ...OptionFunc) ([]*CustomAttribute, *Response, error) { + return s.listCustomAttributes("users", user, options...) +} + +// ListCustomGroupAttributes lists the custom attributes of the specified group. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#list-custom-attributes +func (s *CustomAttributesService) ListCustomGroupAttributes(group int, options ...OptionFunc) ([]*CustomAttribute, *Response, error) { + return s.listCustomAttributes("groups", group, options...) +} + +// ListCustomProjectAttributes lists the custom attributes of the specified project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#list-custom-attributes +func (s *CustomAttributesService) ListCustomProjectAttributes(project int, options ...OptionFunc) ([]*CustomAttribute, *Response, error) { + return s.listCustomAttributes("projects", project, options...) +} + +func (s *CustomAttributesService) listCustomAttributes(resource string, id int, options ...OptionFunc) ([]*CustomAttribute, *Response, error) { + u := fmt.Sprintf("%s/%d/custom_attributes", resource, id) + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var cas []*CustomAttribute + resp, err := s.client.Do(req, &cas) + if err != nil { + return nil, resp, err + } + return cas, resp, err +} + +// GetCustomUserAttribute returns the user attribute with a speciifc key. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#single-custom-attribute +func (s *CustomAttributesService) GetCustomUserAttribute(user int, key string, options ...OptionFunc) (*CustomAttribute, *Response, error) { + return s.getCustomAttribute("users", user, key, options...) +} + +// GetCustomGroupAttribute returns the group attribute with a speciifc key. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#single-custom-attribute +func (s *CustomAttributesService) GetCustomGroupAttribute(group int, key string, options ...OptionFunc) (*CustomAttribute, *Response, error) { + return s.getCustomAttribute("groups", group, key, options...) +} + +// GetCustomProjectAttribute returns the project attribute with a speciifc key. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#single-custom-attribute +func (s *CustomAttributesService) GetCustomProjectAttribute(project int, key string, options ...OptionFunc) (*CustomAttribute, *Response, error) { + return s.getCustomAttribute("projects", project, key, options...) +} + +func (s *CustomAttributesService) getCustomAttribute(resource string, id int, key string, options ...OptionFunc) (*CustomAttribute, *Response, error) { + u := fmt.Sprintf("%s/%d/custom_attributes/%s", resource, id, key) + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var ca *CustomAttribute + resp, err := s.client.Do(req, &ca) + if err != nil { + return nil, resp, err + } + return ca, resp, err +} + +// SetCustomUserAttribute sets the custom attributes of the specified user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#set-custom-attribute +func (s *CustomAttributesService) SetCustomUserAttribute(user int, c CustomAttribute, options ...OptionFunc) (*CustomAttribute, *Response, error) { + return s.setCustomAttribute("users", user, c, options...) +} + +// SetCustomGroupAttribute sets the custom attributes of the specified group. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#set-custom-attribute +func (s *CustomAttributesService) SetCustomGroupAttribute(group int, c CustomAttribute, options ...OptionFunc) (*CustomAttribute, *Response, error) { + return s.setCustomAttribute("groups", group, c, options...) +} + +// SetCustomProjectAttribute sets the custom attributes of the specified project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#set-custom-attribute +func (s *CustomAttributesService) SetCustomProjectAttribute(project int, c CustomAttribute, options ...OptionFunc) (*CustomAttribute, *Response, error) { + return s.setCustomAttribute("projects", project, c, options...) +} + +func (s *CustomAttributesService) setCustomAttribute(resource string, id int, c CustomAttribute, options ...OptionFunc) (*CustomAttribute, *Response, error) { + u := fmt.Sprintf("%s/%d/custom_attributes/%s", resource, id, c.Key) + req, err := s.client.NewRequest("PUT", u, c, options) + if err != nil { + return nil, nil, err + } + + ca := new(CustomAttribute) + resp, err := s.client.Do(req, ca) + if err != nil { + return nil, resp, err + } + return ca, resp, err +} + +// DeleteCustomUserAttribute removes the custom attribute of the specified user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#delete-custom-attribute +func (s *CustomAttributesService) DeleteCustomUserAttribute(user int, key string, options ...OptionFunc) (*Response, error) { + return s.deleteCustomAttribute("users", user, key, options...) +} + +// DeleteCustomGroupAttribute removes the custom attribute of the specified group. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#delete-custom-attribute +func (s *CustomAttributesService) DeleteCustomGroupAttribute(group int, key string, options ...OptionFunc) (*Response, error) { + return s.deleteCustomAttribute("groups", group, key, options...) +} + +// DeleteCustomProjectAttribute removes the custom attribute of the specified project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/custom_attributes.html#delete-custom-attribute +func (s *CustomAttributesService) DeleteCustomProjectAttribute(project int, key string, options ...OptionFunc) (*Response, error) { + return s.deleteCustomAttribute("projects", project, key, options...) +} + +func (s *CustomAttributesService) deleteCustomAttribute(resource string, id int, key string, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("%s/%d/custom_attributes/%s", resource, id, key) + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + return s.client.Do(req, nil) +} diff --git a/api/custom_attributes_test.go b/api/custom_attributes_test.go new file mode 100644 index 0000000..97f8285 --- /dev/null +++ b/api/custom_attributes_test.go @@ -0,0 +1,245 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestListCustomUserAttributes(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/users/2/custom_attributes", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"key":"testkey1", "value":"testvalue1"}, {"key":"testkey2", "value":"testvalue2"}]`) + }) + + customAttributes, _, err := client.CustomAttribute.ListCustomUserAttributes(2) + + if err != nil { + t.Errorf("CustomAttribute.ListCustomUserAttributes returned error: %v", err) + } + + want := []*CustomAttribute{{Key: "testkey1", Value: "testvalue1"}, {Key: "testkey2", Value: "testvalue2"}} + if !reflect.DeepEqual(want, customAttributes) { + t.Errorf("CustomAttribute.ListCustomUserAttributes returned %+v, want %+v", customAttributes, want) + } +} + +func TestListCustomGroupAttributes(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/2/custom_attributes", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"key":"testkey1", "value":"testvalue1"}, {"key":"testkey2", "value":"testvalue2"}]`) + }) + + customAttributes, _, err := client.CustomAttribute.ListCustomGroupAttributes(2) + + if err != nil { + t.Errorf("CustomAttribute.ListCustomGroupAttributes returned error: %v", err) + } + + want := []*CustomAttribute{{Key: "testkey1", Value: "testvalue1"}, {Key: "testkey2", Value: "testvalue2"}} + if !reflect.DeepEqual(want, customAttributes) { + t.Errorf("CustomAttribute.ListCustomGroupAttributes returned %+v, want %+v", customAttributes, want) + } +} + +func TestListCustomProjectAttributes(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/2/custom_attributes", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"key":"testkey1", "value":"testvalue1"}, {"key":"testkey2", "value":"testvalue2"}]`) + }) + + customAttributes, _, err := client.CustomAttribute.ListCustomProjectAttributes(2) + + if err != nil { + t.Errorf("CustomAttribute.ListCustomProjectAttributes returned error: %v", err) + } + + want := []*CustomAttribute{{Key: "testkey1", Value: "testvalue1"}, {Key: "testkey2", Value: "testvalue2"}} + if !reflect.DeepEqual(want, customAttributes) { + t.Errorf("CustomAttribute.ListCustomProjectAttributes returned %+v, want %+v", customAttributes, want) + } +} + +func TestGetCustomUserAttribute(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/users/2/custom_attributes/testkey1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"key":"testkey1", "value":"testvalue1"}`) + }) + + customAttribute, _, err := client.CustomAttribute.GetCustomUserAttribute(2, "testkey1") + + if err != nil { + t.Errorf("CustomAttribute.GetCustomUserAttribute returned error: %v", err) + } + + want := &CustomAttribute{Key: "testkey1", Value: "testvalue1"} + if !reflect.DeepEqual(want, customAttribute) { + t.Errorf("CustomAttribute.GetCustomUserAttribute returned %+v, want %+v", customAttribute, want) + } +} + +func TestGetCustomGropupAttribute(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/2/custom_attributes/testkey1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"key":"testkey1", "value":"testvalue1"}`) + }) + + customAttribute, _, err := client.CustomAttribute.GetCustomGroupAttribute(2, "testkey1") + + if err != nil { + t.Errorf("CustomAttribute.GetCustomGroupAttribute returned error: %v", err) + } + + want := &CustomAttribute{Key: "testkey1", Value: "testvalue1"} + if !reflect.DeepEqual(want, customAttribute) { + t.Errorf("CustomAttribute.GetCustomGroupAttribute returned %+v, want %+v", customAttribute, want) + } +} + +func TestGetCustomProjectAttribute(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/2/custom_attributes/testkey1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"key":"testkey1", "value":"testvalue1"}`) + }) + + customAttribute, _, err := client.CustomAttribute.GetCustomProjectAttribute(2, "testkey1") + + if err != nil { + t.Errorf("CustomAttribute.GetCustomProjectAttribute returned error: %v", err) + } + + want := &CustomAttribute{Key: "testkey1", Value: "testvalue1"} + if !reflect.DeepEqual(want, customAttribute) { + t.Errorf("CustomAttribute.GetCustomProjectAttribute returned %+v, want %+v", customAttribute, want) + } +} + +func TestSetCustomUserAttribute(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/users/2/custom_attributes/testkey1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{"key":"testkey1", "value":"testvalue1"}`) + }) + + customAttribute, _, err := client.CustomAttribute.SetCustomUserAttribute(2, CustomAttribute{ + Key: "testkey1", + Value: "testvalue1", + }) + + if err != nil { + t.Errorf("CustomAttribute.SetCustomUserAttributes returned error: %v", err) + } + + want := &CustomAttribute{Key: "testkey1", Value: "testvalue1"} + if !reflect.DeepEqual(want, customAttribute) { + t.Errorf("CustomAttribute.SetCustomUserAttributes returned %+v, want %+v", customAttribute, want) + } +} + +func TestSetCustomGroupAttribute(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/2/custom_attributes/testkey1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{"key":"testkey1", "value":"testvalue1"}`) + }) + + customAttribute, _, err := client.CustomAttribute.SetCustomGroupAttribute(2, CustomAttribute{ + Key: "testkey1", + Value: "testvalue1", + }) + + if err != nil { + t.Errorf("CustomAttribute.SetCustomGroupAttributes returned error: %v", err) + } + + want := &CustomAttribute{Key: "testkey1", Value: "testvalue1"} + if !reflect.DeepEqual(want, customAttribute) { + t.Errorf("CustomAttribute.SetCustomGroupAttributes returned %+v, want %+v", customAttribute, want) + } +} + +func TestDeleteCustomUserAttribute(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/users/2/custom_attributes/testkey1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusAccepted) + }) + + resp, err := client.CustomAttribute.DeleteCustomUserAttribute(2, "testkey1") + if err != nil { + t.Errorf("CustomAttribute.DeleteCustomUserAttribute returned error: %v", err) + } + + want := http.StatusAccepted + got := resp.StatusCode + if got != want { + t.Errorf("CustomAttribute.DeleteCustomUserAttribute returned %d, want %d", got, want) + } +} + +func TestDeleteCustomGroupAttribute(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/2/custom_attributes/testkey1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusAccepted) + }) + + resp, err := client.CustomAttribute.DeleteCustomGroupAttribute(2, "testkey1") + if err != nil { + t.Errorf("CustomAttribute.DeleteCustomGroupAttribute returned error: %v", err) + } + + want := http.StatusAccepted + got := resp.StatusCode + if got != want { + t.Errorf("CustomAttribute.DeleteCustomGroupAttribute returned %d, want %d", got, want) + } +} + +func TestDeleteCustomProjectAttribute(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/2/custom_attributes/testkey1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusAccepted) + }) + + resp, err := client.CustomAttribute.DeleteCustomProjectAttribute(2, "testkey1") + if err != nil { + t.Errorf("CustomAttribute.DeleteCustomProjectAttribute returned error: %v", err) + } + + want := http.StatusAccepted + got := resp.StatusCode + if got != want { + t.Errorf("CustomAttribute.DeleteCustomProjectAttribute returned %d, want %d", got, want) + } +} diff --git a/api/deploy_keys.go b/api/deploy_keys.go index 840676c..7644459 100644 --- a/api/deploy_keys.go +++ b/api/deploy_keys.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,62 +25,87 @@ import ( // DeployKeysService handles communication with the keys related methods // of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/deploy_keys.html +// GitLab API docs: https://docs.gitlab.com/ce/api/deploy_keys.html type DeployKeysService struct { client *Client } // DeployKey represents a GitLab deploy key. type DeployKey struct { - ID int `json:"id"` - Title string `json:"title"` - Key string `json:"key"` - CreatedAt time.Time `json:"created_at"` + ID int `json:"id"` + Title string `json:"title"` + Key string `json:"key"` + CanPush *bool `json:"can_push"` + CreatedAt *time.Time `json:"created_at"` } func (k DeployKey) String() string { return Stringify(k) } -// ListDeployKeys gets a list of a project's deploy keys +// ListAllDeployKeys gets a list of all deploy keys // // GitLab API docs: -// http://doc.gitlab.com/ce/api/deploy_keys.html#list-deploy-keys -func (s *DeployKeysService) ListDeployKeys(pid interface{}) ([]*DeployKey, *Response, error) { +// https://docs.gitlab.com/ce/api/deploy_keys.html#list-all-deploy-keys +func (s *DeployKeysService) ListAllDeployKeys(options ...OptionFunc) ([]*DeployKey, *Response, error) { + req, err := s.client.NewRequest("GET", "deploy_keys", nil, options) + if err != nil { + return nil, nil, err + } + + var ks []*DeployKey + resp, err := s.client.Do(req, &ks) + if err != nil { + return nil, resp, err + } + + return ks, resp, err +} + +// ListProjectDeployKeysOptions represents the available ListProjectDeployKeys() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/deploy_keys.html#list-project-deploy-keys +type ListProjectDeployKeysOptions ListOptions + +// ListProjectDeployKeys gets a list of a project's deploy keys +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/deploy_keys.html#list-project-deploy-keys +func (s *DeployKeysService) ListProjectDeployKeys(pid interface{}, opt *ListProjectDeployKeysOptions, options ...OptionFunc) ([]*DeployKey, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/keys", url.QueryEscape(project)) + u := fmt.Sprintf("projects/%s/deploy_keys", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } - var k []*DeployKey - resp, err := s.client.Do(req, &k) + var ks []*DeployKey + resp, err := s.client.Do(req, &ks) if err != nil { return nil, resp, err } - return k, resp, err + return ks, resp, err } // GetDeployKey gets a single deploy key. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/deploy_keys.html#single-deploy-key -func (s *DeployKeysService) GetDeployKey( - pid interface{}, - deployKey int) (*DeployKey, *Response, error) { +// https://docs.gitlab.com/ce/api/deploy_keys.html#single-deploy-key +func (s *DeployKeysService) GetDeployKey(pid interface{}, deployKey int, options ...OptionFunc) (*DeployKey, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/keys/%d", url.QueryEscape(project), deployKey) + u := fmt.Sprintf("projects/%s/deploy_keys/%d", url.QueryEscape(project), deployKey) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -97,10 +122,11 @@ func (s *DeployKeysService) GetDeployKey( // AddDeployKeyOptions represents the available ADDDeployKey() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/deploy_keys.html#add-deploy-key +// https://docs.gitlab.com/ce/api/deploy_keys.html#add-deploy-key type AddDeployKeyOptions struct { - Title string `url:"title,omitempty" json:"title,omitempty"` - Key string `url:"key,omitempty" json:"key,omitempty"` + Title *string `url:"title,omitempty" json:"title,omitempty"` + Key *string `url:"key,omitempty" json:"key,omitempty"` + CanPush *bool `url:"can_push,omitempty" json:"can_push,omitempty"` } // AddDeployKey creates a new deploy key for a project. If deploy key already @@ -108,17 +134,15 @@ type AddDeployKeyOptions struct { // original one was is accessible by same user. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/deploy_keys.html#add-deploy-key -func (s *DeployKeysService) AddDeployKey( - pid interface{}, - opt *AddDeployKeyOptions) (*DeployKey, *Response, error) { +// https://docs.gitlab.com/ce/api/deploy_keys.html#add-deploy-key +func (s *DeployKeysService) AddDeployKey(pid interface{}, opt *AddDeployKeyOptions, options ...OptionFunc) (*DeployKey, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/keys", url.QueryEscape(project)) + u := fmt.Sprintf("projects/%s/deploy_keys", url.QueryEscape(project)) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -135,23 +159,43 @@ func (s *DeployKeysService) AddDeployKey( // DeleteDeployKey deletes a deploy key from a project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/deploy_keys.html#delete-deploy-key -func (s *DeployKeysService) DeleteDeployKey(pid interface{}, deployKey int) (*Response, error) { +// https://docs.gitlab.com/ce/api/deploy_keys.html#delete-deploy-key +func (s *DeployKeysService) DeleteDeployKey(pid interface{}, deployKey int, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } - u := fmt.Sprintf("projects/%s/keys/%d", url.QueryEscape(project), deployKey) + u := fmt.Sprintf("projects/%s/deploy_keys/%d", url.QueryEscape(project), deployKey) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + return s.client.Do(req, nil) +} + +// EnableDeployKey enables a deploy key. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/deploy_keys.html#enable-deploy-key +func (s *DeployKeysService) EnableDeployKey(pid interface{}, deployKey int, options ...OptionFunc) (*DeployKey, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/deploy_keys/%d/enable", url.QueryEscape(project), deployKey) + + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { - return resp, err + return nil, nil, err + } + + k := new(DeployKey) + resp, err := s.client.Do(req, k) + if err != nil { + return nil, resp, err } - return resp, err + return k, resp, err } diff --git a/api/deployments.go b/api/deployments.go new file mode 100644 index 0000000..0f0061e --- /dev/null +++ b/api/deployments.go @@ -0,0 +1,121 @@ +// +// Copyright 2018, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// DeploymentsService handles communication with the deployment related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/deployments.html +type DeploymentsService struct { + client *Client +} + +// Deployment represents the Gitlab deployment +type Deployment struct { + ID int `json:"id"` + IID int `json:"iid"` + Ref string `json:"ref"` + SHA string `json:"sha"` + CreatedAt *time.Time `json:"created_at"` + User *ProjectUser `json:"user"` + Environment *Environment `json:"environment"` + Deployable struct { + ID int `json:"id"` + Status string `json:"status"` + Stage string `json:"stage"` + Name string `json:"name"` + Ref string `json:"ref"` + Tag bool `json:"tag"` + Coverage float64 `json:"coverage"` + CreatedAt *time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at"` + FinishedAt *time.Time `json:"finished_at"` + Duration float64 `json:"duration"` + User *User `json:"user"` + Commit *Commit `json:"commit"` + Pipeline struct { + ID int `json:"id"` + SHA string `json:"sha"` + Ref string `json:"ref"` + Status string `json:"status"` + } `json:"pipeline"` + Runner *Runner `json:"runner"` + } `json:"deployable"` +} + +// ListProjectDeploymentsOptions represents the available ListProjectDeployments() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/deployments.html#list-project-deployments +type ListProjectDeploymentsOptions struct { + ListOptions + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + +// ListProjectDeployments gets a list of deployments in a project. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/deployments.html#list-project-deployments +func (s *DeploymentsService) ListProjectDeployments(pid interface{}, opts *ListProjectDeploymentsOptions, options ...OptionFunc) ([]*Deployment, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/deployments", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opts, options) + if err != nil { + return nil, nil, err + } + + var ds []*Deployment + resp, err := s.client.Do(req, &ds) + if err != nil { + return nil, resp, err + } + + return ds, resp, err +} + +// GetProjectDeployment get a deployment for a project. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/deployments.html#get-a-specific-deployment +func (s *DeploymentsService) GetProjectDeployment(pid interface{}, deployment int, options ...OptionFunc) (*Deployment, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/deployments/%d", url.QueryEscape(project), deployment) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + d := new(Deployment) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} diff --git a/api/discussions.go b/api/discussions.go new file mode 100644 index 0000000..d744d35 --- /dev/null +++ b/api/discussions.go @@ -0,0 +1,1113 @@ +// +// Copyright 2018, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// DiscussionsService handles communication with the discussions related +// methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/discussions.html +type DiscussionsService struct { + client *Client +} + +// Discussion represents a GitLab discussion. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/discussions.html +type Discussion struct { + ID string `json:"id"` + IndividualNote bool `json:"individual_note"` + Notes []*Note `json:"notes"` +} + +func (d Discussion) String() string { + return Stringify(d) +} + +// ListIssueDiscussionsOptions represents the available ListIssueDiscussions() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#list-project-issue-discussions +type ListIssueDiscussionsOptions ListOptions + +// ListIssueDiscussions gets a list of all discussions for a single +// issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#list-project-issue-discussions +func (s *DiscussionsService) ListIssueDiscussions(pid interface{}, issue int, opt *ListIssueDiscussionsOptions, options ...OptionFunc) ([]*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/discussions", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var ds []*Discussion + resp, err := s.client.Do(req, &ds) + if err != nil { + return nil, resp, err + } + + return ds, resp, err +} + +// GetIssueDiscussion returns a single discussion for a specific project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#get-single-issue-discussion +func (s *DiscussionsService) GetIssueDiscussion(pid interface{}, issue int, discussion string, options ...OptionFunc) (*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/discussions/%s", + url.QueryEscape(project), + issue, + discussion, + ) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// CreateIssueDiscussionOptions represents the available CreateIssueDiscussion() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#create-new-issue-discussion +type CreateIssueDiscussionOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// CreateIssueDiscussion creates a new discussion to a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#create-new-issue-discussion +func (s *DiscussionsService) CreateIssueDiscussion(pid interface{}, issue int, opt *CreateIssueDiscussionOptions, options ...OptionFunc) (*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/discussions", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// AddIssueDiscussionNoteOptions represents the available AddIssueDiscussionNote() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#add-note-to-existing-issue-discussion +type AddIssueDiscussionNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// AddIssueDiscussionNote creates a new discussion to a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#add-note-to-existing-issue-discussion +func (s *DiscussionsService) AddIssueDiscussionNote(pid interface{}, issue int, discussion string, opt *AddIssueDiscussionNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/discussions/%s/notes", + url.QueryEscape(project), + issue, + discussion, + ) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// UpdateIssueDiscussionNoteOptions represents the available +// UpdateIssueDiscussion() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#modify-existing-issue-discussion-note +type UpdateIssueDiscussionNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// UpdateIssueDiscussionNote modifies existing discussion of an issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#modify-existing-issue-discussion-note +func (s *DiscussionsService) UpdateIssueDiscussionNote(pid interface{}, issue int, discussion string, note int, opt *UpdateIssueDiscussionNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/discussions/%s/notes/%d", + url.QueryEscape(project), + issue, + discussion, + note, + ) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// DeleteIssueDiscussionNote deletes an existing discussion of an issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#delete-an-issue-discussion-note +func (s *DiscussionsService) DeleteIssueDiscussionNote(pid interface{}, issue int, discussion string, note int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/discussions/%s/notes/%d", + url.QueryEscape(project), + issue, + discussion, + note, + ) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ListSnippetDiscussionsOptions represents the available ListSnippetDiscussions() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#list-all-snippet-discussions +type ListSnippetDiscussionsOptions ListOptions + +// ListSnippetDiscussions gets a list of all discussions for a single +// snippet. Snippet discussions are comments users can post to a snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#list-all-snippet-discussions +func (s *DiscussionsService) ListSnippetDiscussions(pid interface{}, snippet int, opt *ListSnippetDiscussionsOptions, options ...OptionFunc) ([]*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/discussions", url.QueryEscape(project), snippet) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var ds []*Discussion + resp, err := s.client.Do(req, &ds) + if err != nil { + return nil, resp, err + } + + return ds, resp, err +} + +// GetSnippetDiscussion returns a single discussion for a given snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#get-single-snippet-discussion +func (s *DiscussionsService) GetSnippetDiscussion(pid interface{}, snippet int, discussion string, options ...OptionFunc) (*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/discussions/%s", + url.QueryEscape(project), + snippet, + discussion, + ) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// CreateSnippetDiscussionOptions represents the available +// CreateSnippetDiscussion() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#create-new-snippet-discussion +type CreateSnippetDiscussionOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// CreateSnippetDiscussion creates a new discussion for a single snippet. +// Snippet discussions are comments users can post to a snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#create-new-snippet-discussion +func (s *DiscussionsService) CreateSnippetDiscussion(pid interface{}, snippet int, opt *CreateSnippetDiscussionOptions, options ...OptionFunc) (*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/discussions", url.QueryEscape(project), snippet) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// AddSnippetDiscussionNoteOptions represents the available +// AddSnippetDiscussionNote() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#add-note-to-existing-snippet-discussion +type AddSnippetDiscussionNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// AddSnippetDiscussionNote creates a new discussion to a single project +// snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#add-note-to-existing-snippet-discussion +func (s *DiscussionsService) AddSnippetDiscussionNote(pid interface{}, snippet int, discussion string, opt *AddSnippetDiscussionNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/discussions/%s/notes", + url.QueryEscape(project), + snippet, + discussion, + ) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// UpdateSnippetDiscussionNoteOptions represents the available +// UpdateSnippetDiscussion() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#modify-existing-snippet-discussion-note +type UpdateSnippetDiscussionNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// UpdateSnippetDiscussionNote modifies existing discussion of a snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#modify-existing-snippet-discussion-note +func (s *DiscussionsService) UpdateSnippetDiscussionNote(pid interface{}, snippet int, discussion string, note int, opt *UpdateSnippetDiscussionNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/discussions/%s/notes/%d", + url.QueryEscape(project), + snippet, + discussion, + note, + ) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// DeleteSnippetDiscussionNote deletes an existing discussion of a snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#delete-a-snippet-discussion-note +func (s *DiscussionsService) DeleteSnippetDiscussionNote(pid interface{}, snippet int, discussion string, note int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/discussions/%s/notes/%d", + url.QueryEscape(project), + snippet, + discussion, + note, + ) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ListGroupEpicDiscussionsOptions represents the available +// ListEpicDiscussions() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#list-all-epic-discussions +type ListGroupEpicDiscussionsOptions ListOptions + +// ListGroupEpicDiscussions gets a list of all discussions for a single +// epic. Epic discussions are comments users can post to a epic. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#list-all-epic-discussions +func (s *DiscussionsService) ListGroupEpicDiscussions(gid interface{}, epic int, opt *ListGroupEpicDiscussionsOptions, options ...OptionFunc) ([]*Discussion, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/epics/%d/discussions", + url.QueryEscape(group), + epic, + ) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var ds []*Discussion + resp, err := s.client.Do(req, &ds) + if err != nil { + return nil, resp, err + } + + return ds, resp, err +} + +// GetEpicDiscussion returns a single discussion for a given epic. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#get-single-epic-discussion +func (s *DiscussionsService) GetEpicDiscussion(gid interface{}, epic int, discussion string, options ...OptionFunc) (*Discussion, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/epics/%d/discussions/%s", + url.QueryEscape(group), + epic, + discussion, + ) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// CreateEpicDiscussionOptions represents the available CreateEpicDiscussion() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#create-new-epic-discussion +type CreateEpicDiscussionOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// CreateEpicDiscussion creates a new discussion for a single epic. Epic +// discussions are comments users can post to a epic. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#create-new-epic-discussion +func (s *DiscussionsService) CreateEpicDiscussion(gid interface{}, epic int, opt *CreateEpicDiscussionOptions, options ...OptionFunc) (*Discussion, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/epics/%d/discussions", + url.QueryEscape(group), + epic, + ) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// AddEpicDiscussionNoteOptions represents the available +// AddEpicDiscussionNote() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#add-note-to-existing-epic-discussion +type AddEpicDiscussionNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// AddEpicDiscussionNote creates a new discussion to a single project epic. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#add-note-to-existing-epic-discussion +func (s *DiscussionsService) AddEpicDiscussionNote(gid interface{}, epic int, discussion string, opt *AddEpicDiscussionNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/epics/%d/discussions/%s/notes", + url.QueryEscape(group), + epic, + discussion, + ) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// UpdateEpicDiscussionNoteOptions represents the available UpdateEpicDiscussion() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#modify-existing-epic-discussion-note +type UpdateEpicDiscussionNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// UpdateEpicDiscussionNote modifies existing discussion of a epic. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#modify-existing-epic-discussion-note +func (s *DiscussionsService) UpdateEpicDiscussionNote(gid interface{}, epic int, discussion string, note int, opt *UpdateEpicDiscussionNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/epics/%d/discussions/%s/notes/%d", + url.QueryEscape(group), + epic, + discussion, + note, + ) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// DeleteEpicDiscussionNote deletes an existing discussion of a epic. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#delete-an-epic-discussion-note +func (s *DiscussionsService) DeleteEpicDiscussionNote(gid interface{}, epic int, discussion string, note int, options ...OptionFunc) (*Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("groups/%s/epics/%d/discussions/%s/notes/%d", + url.QueryEscape(group), + epic, + discussion, + note, + ) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ListMergeRequestDiscussionsOptions represents the available +// ListMergeRequestDiscussions() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#list-all-merge-request-discussions +type ListMergeRequestDiscussionsOptions ListOptions + +// ListMergeRequestDiscussions gets a list of all discussions for a single +// merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#list-all-merge-request-discussions +func (s *DiscussionsService) ListMergeRequestDiscussions(pid interface{}, mergeRequest int, opt *ListMergeRequestDiscussionsOptions, options ...OptionFunc) ([]*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/discussions", + url.QueryEscape(project), + mergeRequest, + ) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var ds []*Discussion + resp, err := s.client.Do(req, &ds) + if err != nil { + return nil, resp, err + } + + return ds, resp, err +} + +// GetMergeRequestDiscussion returns a single discussion for a given merge +// request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#get-single-merge-request-discussion +func (s *DiscussionsService) GetMergeRequestDiscussion(pid interface{}, mergeRequest int, discussion string, options ...OptionFunc) (*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/discussions/%s", + url.QueryEscape(project), + mergeRequest, + discussion, + ) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// CreateMergeRequestDiscussionOptions represents the available +// CreateMergeRequestDiscussion() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#create-new-merge-request-discussion +type CreateMergeRequestDiscussionOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` + Position *NotePosition `url:"position,omitempty" json:"position,omitempty"` +} + +// CreateMergeRequestDiscussion creates a new discussion for a single merge +// request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#create-new-merge-request-discussion +func (s *DiscussionsService) CreateMergeRequestDiscussion(pid interface{}, mergeRequest int, opt *CreateMergeRequestDiscussionOptions, options ...OptionFunc) (*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/discussions", + url.QueryEscape(project), + mergeRequest, + ) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// ResolveMergeRequestDiscussionOptions represents the available +// ResolveMergeRequestDiscussion() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#resolve-a-merge-request-discussion +type ResolveMergeRequestDiscussionOptions struct { + Resolved *bool `url:"resolved,omitempty" json:"resolved,omitempty"` +} + +// ResolveMergeRequestDiscussion resolves/unresolves whole discussion of a merge +// request. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/discussions.html#resolve-a-merge-request-discussion +func (s *DiscussionsService) ResolveMergeRequestDiscussion(pid interface{}, mergeRequest int, discussion string, opt *ResolveMergeRequestDiscussionOptions, options ...OptionFunc) (*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/discussions/%s", + url.QueryEscape(project), + mergeRequest, + discussion, + ) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// AddMergeRequestDiscussionNoteOptions represents the available +// AddMergeRequestDiscussionNote() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#add-note-to-existing-merge-request-discussion +type AddMergeRequestDiscussionNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// AddMergeRequestDiscussionNote creates a new discussion to a single project +// merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#add-note-to-existing-merge-request-discussion +func (s *DiscussionsService) AddMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, opt *AddMergeRequestDiscussionNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/discussions/%s/notes", + url.QueryEscape(project), + mergeRequest, + discussion, + ) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// UpdateMergeRequestDiscussionNoteOptions represents the available +// UpdateMergeRequestDiscussion() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#modify-existing-merge-request-discussion-note +type UpdateMergeRequestDiscussionNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` + Resolved *bool `url:"resolved,omitempty" json:"resolved,omitempty"` +} + +// UpdateMergeRequestDiscussionNote modifies existing discussion of a merge +// request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#modify-existing-merge-request-discussion-note +func (s *DiscussionsService) UpdateMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, note int, opt *UpdateMergeRequestDiscussionNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/discussions/%s/notes/%d", + url.QueryEscape(project), + mergeRequest, + discussion, + note, + ) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// DeleteMergeRequestDiscussionNote deletes an existing discussion of a merge +// request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#delete-a-merge-request-discussion-note +func (s *DiscussionsService) DeleteMergeRequestDiscussionNote(pid interface{}, mergeRequest int, discussion string, note int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/discussions/%s/notes/%d", + url.QueryEscape(project), + mergeRequest, + discussion, + note, + ) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ListCommitDiscussionsOptions represents the available +// ListCommitDiscussions() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#list-project-commit-discussions +type ListCommitDiscussionsOptions ListOptions + +// ListCommitDiscussions gets a list of all discussions for a single +// commit. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#list-project-commit-discussions +func (s *DiscussionsService) ListCommitDiscussions(pid interface{}, commit string, opt *ListCommitDiscussionsOptions, options ...OptionFunc) ([]*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/discussions", + url.QueryEscape(project), + commit, + ) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var ds []*Discussion + resp, err := s.client.Do(req, &ds) + if err != nil { + return nil, resp, err + } + + return ds, resp, err +} + +// GetCommitDiscussion returns a single discussion for a specific project +// commit. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#get-single-commit-discussion +func (s *DiscussionsService) GetCommitDiscussion(pid interface{}, commit string, discussion string, options ...OptionFunc) (*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/discussions/%s", + url.QueryEscape(project), + commit, + discussion, + ) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// CreateCommitDiscussionOptions represents the available +// CreateCommitDiscussion() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#create-new-commit-discussion +type CreateCommitDiscussionOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` + Position *NotePosition `url:"position,omitempty" json:"position,omitempty"` +} + +// CreateCommitDiscussion creates a new discussion to a single project commit. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#create-new-commit-discussion +func (s *DiscussionsService) CreateCommitDiscussion(pid interface{}, commit string, opt *CreateCommitDiscussionOptions, options ...OptionFunc) (*Discussion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/discussions", + url.QueryEscape(project), + commit, + ) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + d := new(Discussion) + resp, err := s.client.Do(req, d) + if err != nil { + return nil, resp, err + } + + return d, resp, err +} + +// AddCommitDiscussionNoteOptions represents the available +// AddCommitDiscussionNote() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#add-note-to-existing-commit-discussion +type AddCommitDiscussionNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// AddCommitDiscussionNote creates a new discussion to a single project commit. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#add-note-to-existing-commit-discussion +func (s *DiscussionsService) AddCommitDiscussionNote(pid interface{}, commit string, discussion string, opt *AddCommitDiscussionNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/discussions/%s/notes", + url.QueryEscape(project), + commit, + discussion, + ) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// UpdateCommitDiscussionNoteOptions represents the available +// UpdateCommitDiscussion() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#modify-existing-commit-discussion-note +type UpdateCommitDiscussionNoteOptions struct { + Body *string `url:"body,omitempty" json:"body,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` +} + +// UpdateCommitDiscussionNote modifies existing discussion of an commit. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#modify-existing-commit-discussion-note +func (s *DiscussionsService) UpdateCommitDiscussionNote(pid interface{}, commit string, discussion string, note int, opt *UpdateCommitDiscussionNoteOptions, options ...OptionFunc) (*Note, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/discussions/%s/notes/%d", + url.QueryEscape(project), + commit, + discussion, + note, + ) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + n := new(Note) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} + +// DeleteCommitDiscussionNote deletes an existing discussion of an commit. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/discussions.html#delete-an-commit-discussion-note +func (s *DiscussionsService) DeleteCommitDiscussionNote(pid interface{}, commit string, discussion string, note int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/repository/commits/%s/discussions/%s/notes/%d", + url.QueryEscape(project), + commit, + discussion, + note, + ) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/environments.go b/api/environments.go new file mode 100644 index 0000000..5afa784 --- /dev/null +++ b/api/environments.go @@ -0,0 +1,185 @@ +// +// Copyright 2017, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// EnvironmentsService handles communication with the environment related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/environments.html +type EnvironmentsService struct { + client *Client +} + +// Environment represents a GitLab environment. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/environments.html +type Environment struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + ExternalURL string `json:"external_url"` +} + +func (env Environment) String() string { + return Stringify(env) +} + +// ListEnvironmentsOptions represents the available ListEnvironments() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/environments.html#list-environments +type ListEnvironmentsOptions ListOptions + +// ListEnvironments gets a list of environments from a project, sorted by name +// alphabetically. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/environments.html#list-environments +func (s *EnvironmentsService) ListEnvironments(pid interface{}, opts *ListEnvironmentsOptions, options ...OptionFunc) ([]*Environment, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/environments", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opts, options) + if err != nil { + return nil, nil, err + } + + var envs []*Environment + resp, err := s.client.Do(req, &envs) + if err != nil { + return nil, resp, err + } + + return envs, resp, err +} + +// CreateEnvironmentOptions represents the available CreateEnvironment() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/environments.html#create-a-new-environment +type CreateEnvironmentOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + ExternalURL *string `url:"external_url,omitempty" json:"external_url,omitempty"` +} + +// CreateEnvironment adds an environment to a project. This is an idempotent +// method and can be called multiple times with the same parameters. Createing +// an environment that is already a environment does not affect the +// existing environmentship. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/environments.html#create-a-new-environment +func (s *EnvironmentsService) CreateEnvironment(pid interface{}, opt *CreateEnvironmentOptions, options ...OptionFunc) (*Environment, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/environments", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + env := new(Environment) + resp, err := s.client.Do(req, env) + if err != nil { + return nil, resp, err + } + + return env, resp, err +} + +// EditEnvironmentOptions represents the available EditEnvironment() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/environments.html#edit-an-existing-environment +type EditEnvironmentOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + ExternalURL *string `url:"external_url,omitempty" json:"external_url,omitempty"` +} + +// EditEnvironment updates a project team environment to a specified access level.. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/environments.html#edit-an-existing-environment +func (s *EnvironmentsService) EditEnvironment(pid interface{}, environment int, opt *EditEnvironmentOptions, options ...OptionFunc) (*Environment, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/environments/%d", url.QueryEscape(project), environment) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + env := new(Environment) + resp, err := s.client.Do(req, env) + if err != nil { + return nil, resp, err + } + + return env, resp, err +} + +// DeleteEnvironment removes an environment from a project team. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/environments.html#remove-a-environment-from-a-group-or-project +func (s *EnvironmentsService) DeleteEnvironment(pid interface{}, environment int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/environments/%d", url.QueryEscape(project), environment) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// StopEnvironment stop an environment from a project team. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/environments.html#stop-an-environment +func (s *EnvironmentsService) StopEnvironment(pid interface{}, environmentID int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/environments/%d/stop", url.QueryEscape(project), environmentID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/environments_test.go b/api/environments_test.go new file mode 100644 index 0000000..329c667 --- /dev/null +++ b/api/environments_test.go @@ -0,0 +1,100 @@ +package gitlab + +import ( + "fmt" + "log" + "net/http" + "reflect" + "testing" +) + +func TestListEnvironments(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/environments", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testURL(t, r, "/api/v4/projects/1/environments?page=1&per_page=10") + fmt.Fprint(w, `[{"id": 1,"name": "review/fix-foo", "slug": "review-fix-foo-dfjre3", "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com"}]`) + }) + + envs, _, err := client.Environments.ListEnvironments(1, &ListEnvironmentsOptions{Page: 1, PerPage: 10}) + if err != nil { + log.Fatal(err) + } + + want := []*Environment{{ID: 1, Name: "review/fix-foo", Slug: "review-fix-foo-dfjre3", ExternalURL: "https://review-fix-foo-dfjre3.example.gitlab.com"}} + if !reflect.DeepEqual(want, envs) { + t.Errorf("Environments.ListEnvironments returned %+v, want %+v", envs, want) + } +} + +func TestCreateEnvironment(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/environments", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testURL(t, r, "/api/v4/projects/1/environments") + fmt.Fprint(w, `{"id": 1,"name": "deploy", "slug": "deploy", "external_url": "https://deploy.example.gitlab.com"}`) + }) + + envs, _, err := client.Environments.CreateEnvironment(1, &CreateEnvironmentOptions{Name: String("deploy"), ExternalURL: String("https://deploy.example.gitlab.com")}) + if err != nil { + log.Fatal(err) + } + + want := &Environment{ID: 1, Name: "deploy", Slug: "deploy", ExternalURL: "https://deploy.example.gitlab.com"} + if !reflect.DeepEqual(want, envs) { + t.Errorf("Environments.CreateEnvironment returned %+v, want %+v", envs, want) + } +} + +func TestEditEnvironment(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/environments/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + testURL(t, r, "/api/v4/projects/1/environments/1") + fmt.Fprint(w, `{"id": 1,"name": "staging", "slug": "staging", "external_url": "https://staging.example.gitlab.com"}`) + }) + + envs, _, err := client.Environments.EditEnvironment(1, 1, &EditEnvironmentOptions{Name: String("staging"), ExternalURL: String("https://staging.example.gitlab.com")}) + if err != nil { + log.Fatal(err) + } + + want := &Environment{ID: 1, Name: "staging", Slug: "staging", ExternalURL: "https://staging.example.gitlab.com"} + if !reflect.DeepEqual(want, envs) { + t.Errorf("Environments.EditEnvironment returned %+v, want %+v", envs, want) + } +} + +func TestDeleteEnvironment(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/environments/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testURL(t, r, "/api/v4/projects/1/environments/1") + }) + _, err := client.Environments.DeleteEnvironment(1, 1) + if err != nil { + log.Fatal(err) + } +} + +func TestStopEnvironment(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/environments/1/stop", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testURL(t, r, "/api/v4/projects/1/environments/1/stop") + }) + _, err := client.Environments.StopEnvironment(1, 1) + if err != nil { + log.Fatal(err) + } +} diff --git a/api/event_parsing.go b/api/event_parsing.go new file mode 100644 index 0000000..df9118c --- /dev/null +++ b/api/event_parsing.go @@ -0,0 +1,114 @@ +package gitlab + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// EventType represents a Gitlab event type. +type EventType string + +// List of available event types. +const ( + EventTypeBuild EventType = "Build Hook" + EventTypeIssue EventType = "Issue Hook" + EventTypeMergeRequest EventType = "Merge Request Hook" + EventTypeNote EventType = "Note Hook" + EventTypePipeline EventType = "Pipeline Hook" + EventTypePush EventType = "Push Hook" + EventTypeTagPush EventType = "Tag Push Hook" + EventTypeWikiPage EventType = "Wiki Page Hook" +) + +const ( + noteableTypeCommit = "Commit" + noteableTypeMergeRequest = "MergeRequest" + noteableTypeIssue = "Issue" + noteableTypeSnippet = "Snippet" +) + +type noteEvent struct { + ObjectKind string `json:"object_kind"` + ObjectAttributes struct { + NoteableType string `json:"noteable_type"` + } `json:"object_attributes"` +} + +const eventTypeHeader = "X-Gitlab-Event" + +// WebhookEventType returns the event type for the given request. +func WebhookEventType(r *http.Request) EventType { + return EventType(r.Header.Get(eventTypeHeader)) +} + +// ParseWebhook parses the event payload. For recognized event types, a +// value of the corresponding struct type will be returned. An error will +// be returned for unrecognized event types. +// +// Example usage: +// +// func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// payload, err := ioutil.ReadAll(r.Body) +// if err != nil { ... } +// event, err := gitlab.ParseWebhook(gitlab.WebhookEventType(r), payload) +// if err != nil { ... } +// switch event := event.(type) { +// case *gitlab.PushEvent: +// processPushEvent(event) +// case *gitlab.MergeEvent: +// processMergeEvent(event) +// ... +// } +// } +// +func ParseWebhook(eventType EventType, payload []byte) (event interface{}, err error) { + switch eventType { + case EventTypeBuild: + event = &BuildEvent{} + case EventTypeIssue: + event = &IssueEvent{} + case EventTypeMergeRequest: + event = &MergeEvent{} + case EventTypePipeline: + event = &PipelineEvent{} + case EventTypePush: + event = &PushEvent{} + case EventTypeTagPush: + event = &TagEvent{} + case EventTypeWikiPage: + event = &WikiPageEvent{} + case EventTypeNote: + note := ¬eEvent{} + err := json.Unmarshal(payload, note) + if err != nil { + return nil, err + } + + if note.ObjectKind != "note" { + return nil, fmt.Errorf("unexpected object kind %s", note.ObjectKind) + } + + switch note.ObjectAttributes.NoteableType { + case noteableTypeCommit: + event = &CommitCommentEvent{} + case noteableTypeMergeRequest: + event = &MergeCommentEvent{} + case noteableTypeIssue: + event = &IssueCommentEvent{} + case noteableTypeSnippet: + event = &SnippetCommentEvent{} + default: + return nil, fmt.Errorf("unexpected noteable type %s", note.ObjectAttributes.NoteableType) + } + + default: + return nil, fmt.Errorf("unexpected event type: %s", eventType) + } + + if err := json.Unmarshal(payload, event); err != nil { + return nil, err + } + + return event, nil +} diff --git a/api/event_parsing_test.go b/api/event_parsing_test.go new file mode 100644 index 0000000..8c3427d --- /dev/null +++ b/api/event_parsing_test.go @@ -0,0 +1,1242 @@ +package gitlab + +import ( + "net/http" + "testing" +) + +func TestWebhookEventType(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "https://gitlab.com", nil) + if err != nil { + t.Errorf("Error creating HTTP request: %s", err) + } + req.Header.Set("X-Gitlab-Event", "Push Hook") + + eventType := WebhookEventType(req) + if eventType != "Push Hook" { + t.Errorf("WebhookEventType is %s, want %s", eventType, "Push Hook") + } +} + +func TestParsePushHook(t *testing.T) { + raw := `{ + "object_kind": "push", + "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", + "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "ref": "refs/heads/master", + "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "user_id": 4, + "user_name": "John Smith", + "user_username": "jsmith", + "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 15, + "project":{ + "id": 15, + "name":"Diaspora", + "description":"", + "web_url":"http://example.com/mike/diaspora", + "avatar_url":null, + "git_ssh_url":"git@example.com:mike/diaspora.git", + "git_http_url":"http://example.com/mike/diaspora.git", + "namespace":"Mike", + "visibility_level":0, + "path_with_namespace":"mike/diaspora", + "default_branch":"master", + "homepage":"http://example.com/mike/diaspora", + "url":"git@example.com:mike/diaspora.git", + "ssh_url":"git@example.com:mike/diaspora.git", + "http_url":"http://example.com/mike/diaspora.git" + }, + "repository":{ + "name": "Diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora", + "git_http_url":"http://example.com/mike/diaspora.git", + "git_ssh_url":"git@example.com:mike/diaspora.git", + "visibility_level":0 + }, + "commits": [ + { + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "message": "Update Catalan translation to e38cb41.", + "timestamp": "2011-12-12T14:27:31+02:00", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "author": { + "name": "Jordi Mallach", + "email": "jordi@softcatala.org" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + }, + { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + } + ], + "total_commits_count": 4 +}` + + parsedEvent, err := ParseWebhook("Push Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing push hook: %s", err) + } + + event, ok := parsedEvent.(*PushEvent) + if !ok { + t.Errorf("Expected PushEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "push" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "push") + } + + if event.ProjectID != 15 { + t.Errorf("ProjectID is %v, want %v", event.ProjectID, 15) + } + + if event.UserName != "John Smith" { + t.Errorf("Username is %s, want %s", event.UserName, "John Smith") + } + + if event.Commits[0] == nil || event.Commits[0].Timestamp == nil { + t.Errorf("Commit Timestamp isn't nil") + } + + if event.Commits[0] == nil || event.Commits[0].Author.Name != "Jordi Mallach" { + t.Errorf("Commit Username is %s, want %s", event.UserName, "Jordi Mallach") + } +} + +func TestParseTagHook(t *testing.T) { + raw := `{ + "object_kind": "tag_push", + "before": "0000000000000000000000000000000000000000", + "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "ref": "refs/tags/v1.0.0", + "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "user_id": 1, + "user_name": "John Smith", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 1, + "project":{ + "id": 1, + "name":"Example", + "description":"", + "web_url":"http://example.com/jsmith/example", + "avatar_url":null, + "git_ssh_url":"git@example.com:jsmith/example.git", + "git_http_url":"http://example.com/jsmith/example.git", + "namespace":"Jsmith", + "visibility_level":0, + "path_with_namespace":"jsmith/example", + "default_branch":"master", + "homepage":"http://example.com/jsmith/example", + "url":"git@example.com:jsmith/example.git", + "ssh_url":"git@example.com:jsmith/example.git", + "http_url":"http://example.com/jsmith/example.git" + }, + "repository":{ + "name": "Example", + "url": "ssh://git@example.com/jsmith/example.git", + "description": "", + "homepage": "http://example.com/jsmith/example", + "git_http_url":"http://example.com/jsmith/example.git", + "git_ssh_url":"git@example.com:jsmith/example.git", + "visibility_level":0 + }, + "commits": [], + "total_commits_count": 0 +}` + + parsedEvent, err := ParseWebhook("Tag Push Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing tag hook: %s", err) + } + + event, ok := parsedEvent.(*TagEvent) + if !ok { + t.Errorf("Expected TagEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "tag_push" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "tag_push") + } + + if event.ProjectID != 1 { + t.Errorf("ProjectID is %v, want %v", event.ProjectID, 1) + } + + if event.UserName != "John Smith" { + t.Errorf("Username is %s, want %s", event.UserName, "John Smith") + } + + if event.Ref != "refs/tags/v1.0.0" { + t.Errorf("Ref is %s, want %s", event.Ref, "refs/tags/v1.0.0") + } +} + +func TestParseIssueHook(t *testing.T) { + raw := `{ + "object_kind": "issue", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project": { + "id": 1, + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlabhq/gitlab-test" + }, + "object_attributes": { + "id": 301, + "title": "New API: create/update/delete file", + "assignee_ids": [51], + "assignee_id": 51, + "author_id": 51, + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "position": 0, + "branch_name": null, + "description": "Create new API for manipulations with repository", + "milestone_id": null, + "state": "opened", + "iid": 23, + "url": "http://example.com/diaspora/issues/23", + "action": "open" + }, + "assignees": [{ + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }], + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "labels": [{ + "id": 206, + "title": "API", + "color": "#ffffff", + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "template": false, + "description": "API related issues", + "type": "ProjectLabel", + "group_id": 41 + }], + "changes": { + "updated_by_id": [null, 1], + "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"], + "labels": { + "previous": [{ + "id": 206, + "title": "API", + "color": "#ffffff", + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "template": false, + "description": "API related issues", + "type": "ProjectLabel", + "group_id": 41 + }], + "current": [{ + "id": 205, + "title": "Platform", + "color": "#123123", + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "template": false, + "description": "Platform related issues", + "type": "ProjectLabel", + "group_id": 41 + }] + } + } +}` + + parsedEvent, err := ParseWebhook("Issue Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing issue hook: %s", err) + } + + event, ok := parsedEvent.(*IssueEvent) + if !ok { + t.Errorf("Expected IssueEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "issue" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "issue") + } + + if event.Project.Name != "Gitlab Test" { + t.Errorf("Project name is %v, want %v", event.Project.Name, "Gitlab Test") + } + + if event.ObjectAttributes.State != "opened" { + t.Errorf("Issue state is %v, want %v", event.ObjectAttributes.State, "opened") + } + + if event.Assignee.Username != "user1" { + t.Errorf("Assignee username is %v, want %v", event.Assignee.Username, "user1") + } +} + +func TestParseCommitCommentHook(t *testing.T) { + raw := `{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "id": 5, + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository":{ + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1243, + "note": "This is a commit comment. How does this work?", + "noteable_type": "Commit", + "author_id": 1, + "created_at": "2015-05-17 18:08:09 UTC", + "updated_at": "2015-05-17 18:08:09 UTC", + "project_id": 5, + "attachment":null, + "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1", + "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "noteable_id": null, + "system": false, + "st_diff": { + "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n", + "new_path": "six", + "old_path": "six", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false + }, + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243" + }, + "commit": { + "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "timestamp": "2014-02-27T10:06:20+02:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + } +}` + + parsedEvent, err := ParseWebhook("Note Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing note hook: %s", err) + } + + event, ok := parsedEvent.(*CommitCommentEvent) + if !ok { + t.Errorf("Expected CommitCommentEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "note" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "note") + } + + if event.ProjectID != 5 { + t.Errorf("ProjectID is %v, want %v", event.ProjectID, 5) + } + + if event.ObjectAttributes.NoteableType != "Commit" { + t.Errorf("NoteableType is %v, want %v", event.ObjectAttributes.NoteableType, "Commit") + } + + if event.Commit.ID != "cfe32cf61b73a0d5e9f13e774abde7ff789b1660" { + t.Errorf("CommitID is %v, want %v", event.Commit.ID, "cfe32cf61b73a0d5e9f13e774abde7ff789b1660") + } +} + +func TestParseMergeRequestCommentHook(t *testing.T) { + raw := `{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "id": 5, + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name": "Gitlab Test", + "url": "http://localhost/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1244, + "note": "This MR needs work.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2015-05-17 18:21:36 UTC", + "updated_at": "2015-05-17 18:21:36 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 7, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244" + }, + "merge_request": { + "id": 7, + "target_branch": "markdown", + "source_branch": "master", + "source_project_id": 5, + "author_id": 8, + "assignee_id": 28, + "title": "Tempora et eos debitis quae laborum et.", + "created_at": "2015-03-01 20:12:53 UTC", + "updated_at": "2015-03-21 18:27:27 UTC", + "milestone_id": 11, + "state": "opened", + "merge_status": "cannot_be_merged", + "target_project_id": 5, + "iid": 1, + "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.", + "position": 0, + "source":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "target": { + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "last_commit": { + "id": "562e173be03b8ff2efb05345d12df18815438a4b", + "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n", + "timestamp": "2015-04-08T21:00:25-07:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b", + "author": { + "name": "John Smith", + "email": "john@example.com" + } + }, + "work_in_progress": false, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } + } +}` + + parsedEvent, err := ParseWebhook("Note Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing note hook: %s", err) + } + + event, ok := parsedEvent.(*MergeCommentEvent) + if !ok { + t.Errorf("Expected MergeCommentEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "note" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "note") + } + + if event.ProjectID != 5 { + t.Errorf("ProjectID is %v, want %v", event.ProjectID, 5) + } + + if event.ObjectAttributes.NoteableType != "MergeRequest" { + t.Errorf("NoteableType is %v, want %v", event.ObjectAttributes.NoteableType, "MergeRequest") + } + + if event.MergeRequest.ID != 7 { + t.Errorf("MergeRequest ID is %v, want %v", event.MergeRequest.ID, 7) + } +} + +func TestParseIssueCommentHook(t *testing.T) { + raw := `{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "id": 5, + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name":"diaspora", + "url":"git@example.com:mike/diaspora.git", + "description":"", + "homepage":"http://example.com/mike/diaspora" + }, + "object_attributes": { + "id": 1241, + "note": "Hello world", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2015-05-17 17:06:40 UTC", + "updated_at": "2015-05-17 17:06:40 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 92, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" + }, + "issue": { + "id": 92, + "title": "test", + "assignee_ids": [], + "assignee_id": null, + "author_id": 1, + "project_id": 5, + "created_at": "2016-01-04T15:31:46.176Z", + "updated_at": "2016-01-04T15:31:46.176Z", + "position": 0, + "branch_name": null, + "description": "test", + "milestone_id": null, + "state": "closed", + "iid": 17 + } +}` + + parsedEvent, err := ParseWebhook("Note Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing note hook: %s", err) + } + + event, ok := parsedEvent.(*IssueCommentEvent) + if !ok { + t.Errorf("Expected IssueCommentEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "note" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "note") + } + + if event.ProjectID != 5 { + t.Errorf("ProjectID is %v, want %v", event.ProjectID, 5) + } + + if event.ObjectAttributes.NoteableType != "Issue" { + t.Errorf("NoteableType is %v, want %v", event.ObjectAttributes.NoteableType, "Issue") + } + + if event.Issue.Title != "test" { + t.Errorf("Issue title is %v, want %v", event.Issue.Title, "test") + } +} + +func TestParseSnippetCommentHook(t *testing.T) { + raw := `{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "id": 5, + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name":"Gitlab Test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "description":"Aut reprehenderit ut est.", + "homepage":"http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1245, + "note": "Is this snippet doing what it's supposed to be doing?", + "noteable_type": "Snippet", + "author_id": 1, + "created_at": "2015-05-17 18:35:50 UTC", + "updated_at": "2015-05-17 18:35:50 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 53, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245" + }, + "snippet": { + "id": 53, + "title": "test", + "content": "puts 'Hello world'", + "author_id": 1, + "project_id": 5, + "created_at": "2016-01-04T15:31:46.176Z", + "updated_at": "2016-01-04T15:31:46.176Z", + "file_name": "test.rb", + "expires_at": null, + "type": "ProjectSnippet", + "visibility_level": 0 + } +}` + + parsedEvent, err := ParseWebhook("Note Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing note hook: %s", err) + } + + event, ok := parsedEvent.(*SnippetCommentEvent) + if !ok { + t.Errorf("Expected SnippetCommentEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "note" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "note") + } + + if event.ProjectID != 5 { + t.Errorf("ProjectID is %v, want %v", event.ProjectID, 5) + } + + if event.ObjectAttributes.NoteableType != "Snippet" { + t.Errorf("NoteableType is %v, want %v", event.ObjectAttributes.NoteableType, "Snippet") + } + + if event.Snippet.Title != "test" { + t.Errorf("Snippet title is %v, want %v", event.Snippet.Title, "test") + } +} + +func TestParseMergeRequestHook(t *testing.T) { + raw := `{ + "object_kind": "merge_request", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project": { + "id": 1, + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlabhq/gitlab-test" + }, + "object_attributes": { + "id": 99, + "target_branch": "master", + "source_branch": "ms-viewport", + "source_project_id": 14, + "author_id": 51, + "assignee_id": 6, + "title": "MS-Viewport", + "created_at": "2013-12-03T17:23:34Z", + "updated_at": "2013-12-03T17:23:34Z", + "milestone_id": null, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 14, + "iid": 1, + "description": "", + "source": { + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" + }, + "target": { + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" + }, + "last_commit": { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + } + }, + "work_in_progress": false, + "url": "http://example.com/diaspora/merge_requests/1", + "action": "open", + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } + }, + "labels": [{ + "id": 206, + "title": "API", + "color": "#ffffff", + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "template": false, + "description": "API related issues", + "type": "ProjectLabel", + "group_id": 41 + }], + "changes": { + "updated_by_id": { + "previous": null, + "current": 1 + }, + "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"], + "labels": { + "previous": [{ + "id": 206, + "title": "API", + "color": "#ffffff", + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "template": false, + "description": "API related issues", + "type": "ProjectLabel", + "group_id": 41 + }], + "current": [{ + "id": 205, + "title": "Platform", + "color": "#123123", + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "template": false, + "description": "Platform related issues", + "type": "ProjectLabel", + "group_id": 41 + }] + } + } +}` + + parsedEvent, err := ParseWebhook("Merge Request Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing merge request hook: %s", err) + } + + event, ok := parsedEvent.(*MergeEvent) + if !ok { + t.Errorf("Expected MergeEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "merge_request" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "merge_request") + } + + if event.ObjectAttributes.MergeStatus != "unchecked" { + t.Errorf("MergeStatus is %v, want %v", event.ObjectAttributes.MergeStatus, "unchecked") + } + + if event.ObjectAttributes.LastCommit.ID != "da1560886d4f094c3e6c9ef40349f7d38b5d27d7" { + t.Errorf("LastCommit ID is %v, want %v", event.ObjectAttributes.LastCommit.ID, "da1560886d4f094c3e6c9ef40349f7d38b5d27d7") + } + + if event.ObjectAttributes.WorkInProgress { + t.Errorf("WorkInProgress is %v, want %v", event.ObjectAttributes.WorkInProgress, false) + } +} + +func TestParseWikiPageHook(t *testing.T) { + raw := `{ + "object_kind": "wiki_page", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + }, + "project": { + "id": 1, + "name": "awesome-project", + "description": "This is awesome", + "web_url": "http://example.com/root/awesome-project", + "avatar_url": null, + "git_ssh_url": "git@example.com:root/awesome-project.git", + "git_http_url": "http://example.com/root/awesome-project.git", + "namespace": "root", + "visibility_level": 0, + "path_with_namespace": "root/awesome-project", + "default_branch": "master", + "homepage": "http://example.com/root/awesome-project", + "url": "git@example.com:root/awesome-project.git", + "ssh_url": "git@example.com:root/awesome-project.git", + "http_url": "http://example.com/root/awesome-project.git" + }, + "wiki": { + "web_url": "http://example.com/root/awesome-project/wikis/home", + "git_ssh_url": "git@example.com:root/awesome-project.wiki.git", + "git_http_url": "http://example.com/root/awesome-project.wiki.git", + "path_with_namespace": "root/awesome-project.wiki", + "default_branch": "master" + }, + "object_attributes": { + "title": "Awesome", + "content": "awesome content goes here", + "format": "markdown", + "message": "adding an awesome page to the wiki", + "slug": "awesome", + "url": "http://example.com/root/awesome-project/wikis/awesome", + "action": "create" + } +}` + + parsedEvent, err := ParseWebhook("Wiki Page Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing wiki page hook: %s", err) + } + + event, ok := parsedEvent.(*WikiPageEvent) + if !ok { + t.Errorf("Expected WikiPageEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "wiki_page" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "wiki_page") + } + + if event.Project.Name != "awesome-project" { + t.Errorf("Project name is %v, want %v", event.Project.Name, "awesome-project") + } + + if event.Wiki.WebURL != "http://example.com/root/awesome-project/wikis/home" { + t.Errorf("Wiki web URL is %v, want %v", event.Wiki.WebURL, "http://example.com/root/awesome-project/wikis/home") + } + + if event.ObjectAttributes.Message != "adding an awesome page to the wiki" { + t.Errorf("Message is %v, want %v", event.ObjectAttributes.Message, "adding an awesome page to the wiki") + } +} + +func TestParsePipelineHook(t *testing.T) { + raw := `{ + "object_kind": "pipeline", + "object_attributes":{ + "id": 31, + "ref": "master", + "tag": false, + "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "status": "success", + "stages":[ + "build", + "test", + "deploy" + ], + "created_at": "2016-08-12 15:23:28 UTC", + "finished_at": "2016-08-12 15:26:29 UTC", + "duration": 63, + "variables": [ + { + "key": "NESTOR_PROD_ENVIRONMENT", + "value": "us-west-1" + } + ] + }, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "project":{ + "id": 1, + "name": "Gitlab Test", + "description": "Atque in sunt eos similique dolores voluptatem.", + "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test", + "avatar_url": null, + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 20, + "path_with_namespace": "gitlab-org/gitlab-test", + "default_branch": "master" + }, + "commit":{ + "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "message": "test\n", + "timestamp": "2016-08-12T17:23:21+02:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "author":{ + "name": "User", + "email": "user@gitlab.com" + } + }, + "builds":[ + { + "id": 380, + "stage": "deploy", + "name": "production", + "status": "skipped", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": null, + "finished_at": null, + "when": "manual", + "manual": true, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 377, + "stage": "test", + "name": "test-image", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:26:12 UTC", + "finished_at": null, + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 378, + "stage": "test", + "name": "test-build", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:26:12 UTC", + "finished_at": "2016-08-12 15:26:29 UTC", + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 376, + "stage": "build", + "name": "build-image", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:24:56 UTC", + "finished_at": "2016-08-12 15:25:26 UTC", + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 379, + "stage": "deploy", + "name": "staging", + "status": "created", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": null, + "finished_at": null, + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + } + ] +}` + + parsedEvent, err := ParseWebhook("Pipeline Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing pipeline hook: %s", err) + } + + event, ok := parsedEvent.(*PipelineEvent) + if !ok { + t.Errorf("Expected PipelineEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "pipeline" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "pipeline") + } + + if event.ObjectAttributes.Duration != 63 { + t.Errorf("Duration is %v, want %v", event.ObjectAttributes.Duration, 63) + } + + if event.Commit.ID != "bcbb5ec396a2c0f828686f14fac9b80b780504f2" { + t.Errorf("Commit ID is %v, want %v", event.Commit.ID, "bcbb5ec396a2c0f828686f14fac9b80b780504f2") + } + + if event.Builds[0].ID != 380 { + t.Errorf("Builds[0] ID is %v, want %v", event.Builds[0].ID, 380) + } +} + +func TestParseBuildHook(t *testing.T) { + raw := `{ + "object_kind": "build", + "ref": "gitlab-script-trigger", + "tag": false, + "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "build_id": 1977, + "build_name": "test", + "build_stage": "test", + "build_status": "created", + "build_started_at": null, + "build_finished_at": null, + "build_duration": null, + "build_allow_failure": false, + "build_failure_reason": "script_failure", + "project_id": 380, + "project_name": "gitlab-org/gitlab-test", + "user": { + "id": 3, + "name": "User", + "email": "user@gitlab.com" + }, + "commit": { + "id": 2366, + "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "message": "test\n", + "author_name": "User", + "author_email": "user@gitlab.com", + "status": "created", + "duration": null, + "started_at": null, + "finished_at": null + }, + "repository": { + "name": "gitlab_test", + "description": "Atque in sunt eos similique dolores voluptatem.", + "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test", + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", + "visibility_level": 20 + } +}` + + parsedEvent, err := ParseWebhook("Build Hook", []byte(raw)) + if err != nil { + t.Errorf("Error parsing build hook: %s", err) + } + + event, ok := parsedEvent.(*BuildEvent) + if !ok { + t.Errorf("Expected BuildEvent, but parsing produced %T", parsedEvent) + } + + if event.ObjectKind != "build" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "build") + } + + if event.BuildID != 1977 { + t.Errorf("BuildID is %v, want %v", event.BuildID, 1977) + } + + if event.BuildAllowFailure { + t.Errorf("BuildAllowFailure is %v, want %v", event.BuildAllowFailure, false) + } + + if event.Commit.SHA != "2293ada6b400935a1378653304eaf6221e0fdb8f" { + t.Errorf("Commit SHA is %v, want %v", event.Commit.SHA, "2293ada6b400935a1378653304eaf6221e0fdb8f") + } +} diff --git a/api/event_types.go b/api/event_types.go new file mode 100644 index 0000000..f9c2865 --- /dev/null +++ b/api/event_types.go @@ -0,0 +1,670 @@ +// +// Copyright 2017, Sander van Harmelen +// +// 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 gitlab + +import ( + "time" +) + +// PushEvent represents a push event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#push-events +type PushEvent struct { + ObjectKind string `json:"object_kind"` + Before string `json:"before"` + After string `json:"after"` + Ref string `json:"ref"` + CheckoutSHA string `json:"checkout_sha"` + UserID int `json:"user_id"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` + UserAvatar string `json:"user_avatar"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + Visibility VisibilityValue `json:"visibility"` + } `json:"project"` + Repository *Repository `json:"repository"` + Commits []*struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp *time.Time `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + Added []string `json:"added"` + Modified []string `json:"modified"` + Removed []string `json:"removed"` + } `json:"commits"` + TotalCommitsCount int `json:"total_commits_count"` +} + +// TagEvent represents a tag event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#tag-events +type TagEvent struct { + ObjectKind string `json:"object_kind"` + Before string `json:"before"` + After string `json:"after"` + Ref string `json:"ref"` + CheckoutSHA string `json:"checkout_sha"` + UserID int `json:"user_id"` + UserName string `json:"user_name"` + UserAvatar string `json:"user_avatar"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + Visibility VisibilityValue `json:"visibility"` + } `json:"project"` + Repository *Repository `json:"repository"` + Commits []*struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp *time.Time `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + Added []string `json:"added"` + Modified []string `json:"modified"` + Removed []string `json:"removed"` + } `json:"commits"` + TotalCommitsCount int `json:"total_commits_count"` +} + +// IssueEvent represents a issue event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#issues-events +type IssueEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + Visibility VisibilityValue `json:"visibility"` + } `json:"project"` + Repository *Repository `json:"repository"` + ObjectAttributes struct { + ID int `json:"id"` + Title string `json:"title"` + AssigneeID int `json:"assignee_id"` + AuthorID int `json:"author_id"` + ProjectID int `json:"project_id"` + CreatedAt string `json:"created_at"` // Should be *time.Time (see Gitlab issue #21468) + UpdatedAt string `json:"updated_at"` // Should be *time.Time (see Gitlab issue #21468) + Position int `json:"position"` + BranchName string `json:"branch_name"` + Description string `json:"description"` + MilestoneID int `json:"milestone_id"` + State string `json:"state"` + IID int `json:"iid"` + URL string `json:"url"` + Action string `json:"action"` + } `json:"object_attributes"` + Assignee struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"assignee"` +} + +// CommitCommentEvent represents a comment on a commit event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#comment-on-commit +type CommitCommentEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + Visibility VisibilityValue `json:"visibility"` + } `json:"project"` + Repository *Repository `json:"repository"` + 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 string `json:"attachment"` + LineCode string `json:"line_code"` + CommitID string `json:"commit_id"` + NoteableID int `json:"noteable_id"` + System bool `json:"system"` + StDiff struct { + Diff string `json:"diff"` + NewPath string `json:"new_path"` + OldPath string `json:"old_path"` + AMode string `json:"a_mode"` + BMode string `json:"b_mode"` + NewFile bool `json:"new_file"` + RenamedFile bool `json:"renamed_file"` + DeletedFile bool `json:"deleted_file"` + } `json:"st_diff"` + } `json:"object_attributes"` + Commit *struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp *time.Time `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + } `json:"commit"` +} + +// MergeCommentEvent represents a comment on a merge event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#comment-on-merge-request +type MergeCommentEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + Visibility VisibilityValue `json:"visibility"` + } `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 string `json:"attachment"` + LineCode string `json:"line_code"` + CommitID string `json:"commit_id"` + NoteableID int `json:"noteable_id"` + System bool `json:"system"` + StDiff *Diff `json:"st_diff"` + URL string `json:"url"` + } `json:"object_attributes"` + Repository *Repository `json:"repository"` + MergeRequest struct { + ID int `json:"id"` + TargetBranch string `json:"target_branch"` + SourceBranch string `json:"source_branch"` + SourceProjectID int `json:"source_project_id"` + AuthorID int `json:"author_id"` + AssigneeID int `json:"assignee_id"` + Title string `json:"title"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + MilestoneID int `json:"milestone_id"` + State string `json:"state"` + MergeStatus string `json:"merge_status"` + TargetProjectID int `json:"target_project_id"` + IID int `json:"iid"` + Description string `json:"description"` + Position int `json:"position"` + LockedAt string `json:"locked_at"` + UpdatedByID int `json:"updated_by_id"` + MergeError string `json:"merge_error"` + MergeParams struct { + ForceRemoveSourceBranch string `json:"force_remove_source_branch"` + } `json:"merge_params"` + MergeWhenPipelineSucceeds bool `json:"merge_when_pipeline_succeeds"` + MergeUserID int `json:"merge_user_id"` + MergeCommitSHA string `json:"merge_commit_sha"` + DeletedAt string `json:"deleted_at"` + InProgressMergeCommitSHA string `json:"in_progress_merge_commit_sha"` + LockVersion int `json:"lock_version"` + ApprovalsBeforeMerge string `json:"approvals_before_merge"` + RebaseCommitSHA string `json:"rebase_commit_sha"` + TimeEstimate int `json:"time_estimate"` + Squash bool `json:"squash"` + LastEditedAt string `json:"last_edited_at"` + LastEditedByID int `json:"last_edited_by_id"` + Source *Repository `json:"source"` + Target *Repository `json:"target"` + LastCommit struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp *time.Time `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"` + } `json:"merge_request"` +} + +// IssueCommentEvent represents a comment on an issue event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#comment-on-issue +type IssueCommentEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + Visibility VisibilityValue `json:"visibility"` + } `json:"project"` + Repository *Repository `json:"repository"` + 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 string `json:"attachment"` + LineCode string `json:"line_code"` + CommitID string `json:"commit_id"` + NoteableID int `json:"noteable_id"` + System bool `json:"system"` + StDiff []*Diff `json:"st_diff"` + URL string `json:"url"` + } `json:"object_attributes"` + Issue *Issue `json:"issue"` +} + +// SnippetCommentEvent represents a comment on a snippet event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#comment-on-code-snippet +type SnippetCommentEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + ProjectID int `json:"project_id"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + Visibility VisibilityValue `json:"visibility"` + } `json:"project"` + Repository *Repository `json:"repository"` + 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 string `json:"attachment"` + LineCode string `json:"line_code"` + CommitID string `json:"commit_id"` + NoteableID int `json:"noteable_id"` + System bool `json:"system"` + StDiff *Diff `json:"st_diff"` + URL string `json:"url"` + } `json:"object_attributes"` + Snippet *Snippet `json:"snippet"` +} + +// MergeEvent represents a merge event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#merge-request-events +type MergeEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + Project struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + Visibility VisibilityValue `json:"visibility"` + } `json:"project"` + ObjectAttributes struct { + ID int `json:"id"` + TargetBranch string `json:"target_branch"` + SourceBranch string `json:"source_branch"` + SourceProjectID int `json:"source_project_id"` + AuthorID int `json:"author_id"` + AssigneeID int `json:"assignee_id"` + Title string `json:"title"` + CreatedAt string `json:"created_at"` // Should be *time.Time (see Gitlab issue #21468) + UpdatedAt string `json:"updated_at"` // Should be *time.Time (see Gitlab issue #21468) + StCommits []*Commit `json:"st_commits"` + StDiffs []*Diff `json:"st_diffs"` + MilestoneID int `json:"milestone_id"` + State string `json:"state"` + MergeStatus string `json:"merge_status"` + TargetProjectID int `json:"target_project_id"` + IID int `json:"iid"` + Description string `json:"description"` + Position int `json:"position"` + LockedAt string `json:"locked_at"` + UpdatedByID int `json:"updated_by_id"` + MergeError string `json:"merge_error"` + MergeParams struct { + ForceRemoveSourceBranch string `json:"force_remove_source_branch"` + } `json:"merge_params"` + MergeWhenBuildSucceeds bool `json:"merge_when_build_succeeds"` + MergeUserID int `json:"merge_user_id"` + MergeCommitSHA string `json:"merge_commit_sha"` + DeletedAt string `json:"deleted_at"` + ApprovalsBeforeMerge string `json:"approvals_before_merge"` + RebaseCommitSHA string `json:"rebase_commit_sha"` + InProgressMergeCommitSHA string `json:"in_progress_merge_commit_sha"` + LockVersion int `json:"lock_version"` + TimeEstimate int `json:"time_estimate"` + Source *Repository `json:"source"` + Target *Repository `json:"target"` + LastCommit struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp *time.Time `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"` + URL string `json:"url"` + Action string `json:"action"` + OldRev string `json:"oldrev"` + Assignee struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"assignee"` + } `json:"object_attributes"` + Repository *Repository `json:"repository"` + Assignee struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"assignee"` + Changes struct { + AssigneeID struct { + Previous int `json:"previous"` + Current int `json:"current"` + } `json:"assignee_id"` + Description struct { + Previous string `json:"previous"` + Current string `json:"current"` + } `json:"description"` + Labels struct { + Previous []Label `json:"previous"` + Current []Label `json:"current"` + } `json:"labels"` + UpdatedByID struct { + Previous int `json:"previous"` + Current int `json:"current"` + } `json:"updated_by_id"` + } `json:"changes"` +} + +// WikiPageEvent represents a wiki page event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#wiki-page-events +type WikiPageEvent struct { + ObjectKind string `json:"object_kind"` + User *User `json:"user"` + Project struct { + Name string `json:"name"` + Description string `json:"description"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + Visibility VisibilityValue `json:"visibility"` + } `json:"project"` + Wiki struct { + WebURL string `json:"web_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + } `json:"wiki"` + ObjectAttributes struct { + Title string `json:"title"` + Content string `json:"content"` + Format string `json:"format"` + Message string `json:"message"` + Slug string `json:"slug"` + URL string `json:"url"` + Action string `json:"action"` + } `json:"object_attributes"` +} + +// PipelineEvent represents a pipeline event. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#pipeline-events +type PipelineEvent struct { + ObjectKind string `json:"object_kind"` + ObjectAttributes struct { + ID int `json:"id"` + Ref string `json:"ref"` + Tag bool `json:"tag"` + SHA string `json:"sha"` + BeforeSHA string `json:"before_sha"` + Status string `json:"status"` + Stages []string `json:"stages"` + CreatedAt string `json:"created_at"` + FinishedAt string `json:"finished_at"` + Duration int `json:"duration"` + } `json:"object_attributes"` + 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"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` + WebURL string `json:"web_url"` + Visibility VisibilityValue `json:"visibility"` + } `json:"project"` + Commit struct { + ID string `json:"id"` + Message string `json:"message"` + Timestamp *time.Time `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"author"` + } `json:"commit"` + Builds []struct { + ID int `json:"id"` + Stage string `json:"stage"` + Name string `json:"name"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + StartedAt string `json:"started_at"` + FinishedAt string `json:"finished_at"` + When string `json:"when"` + Manual bool `json:"manual"` + User struct { + Name string `json:"name"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` + } `json:"user"` + Runner struct { + ID int `json:"id"` + Description string `json:"description"` + Active bool `json:"active"` + IsShared bool `json:"is_shared"` + } `json:"runner"` + ArtifactsFile struct { + Filename string `json:"filename"` + Size int `json:"size"` + } `json:"artifacts_file"` + } `json:"builds"` +} + +//BuildEvent represents a build event +// +// GitLab API docs: +// https://docs.gitlab.com/ce/web_hooks/web_hooks.html#build-events +type BuildEvent struct { + ObjectKind string `json:"object_kind"` + Ref string `json:"ref"` + Tag bool `json:"tag"` + BeforeSHA string `json:"before_sha"` + SHA string `json:"sha"` + BuildID int `json:"build_id"` + BuildName string `json:"build_name"` + BuildStage string `json:"build_stage"` + BuildStatus string `json:"build_status"` + BuildStartedAt string `json:"build_started_at"` + BuildFinishedAt string `json:"build_finished_at"` + BuildDuration float64 `json:"build_duration"` + BuildAllowFailure bool `json:"build_allow_failure"` + ProjectID int `json:"project_id"` + ProjectName string `json:"project_name"` + User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + } `json:"user"` + Commit struct { + ID int `json:"id"` + SHA string `json:"sha"` + Message string `json:"message"` + AuthorName string `json:"author_name"` + AuthorEmail string `json:"author_email"` + Status string `json:"status"` + Duration int `json:"duration"` + StartedAt string `json:"started_at"` + FinishedAt string `json:"finished_at"` + } `json:"commit"` + Repository *Repository `json:"repository"` +} diff --git a/api/event_types_test.go b/api/event_types_test.go new file mode 100644 index 0000000..ef81a6c --- /dev/null +++ b/api/event_types_test.go @@ -0,0 +1,643 @@ +package gitlab + +import ( + "encoding/json" + "testing" +) + +func TestPushEventUnmarshal(t *testing.T) { + jsonObject := `{ + "object_kind": "push", + "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", + "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "ref": "refs/heads/master", + "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "user_id": 4, + "user_name": "John Smith", + "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 15, + "project":{ + "name":"Diaspora", + "description":"", + "web_url":"http://example.com/mike/diaspora", + "avatar_url":null, + "git_ssh_url":"git@example.com:mike/diaspora.git", + "git_http_url":"http://example.com/mike/diaspora.git", + "namespace":"Mike", + "visibility":"public", + "path_with_namespace":"mike/diaspora", + "default_branch":"master", + "homepage":"http://example.com/mike/diaspora", + "url":"git@example.com:mike/diaspora.git", + "ssh_url":"git@example.com:mike/diaspora.git", + "http_url":"http://example.com/mike/diaspora.git" + }, + "repository":{ + "name": "Diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora", + "git_http_url":"http://example.com/mike/diaspora.git", + "git_ssh_url":"git@example.com:mike/diaspora.git", + "visibility":"public" + }, + "commits": [ + { + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "message": "Update Catalan translation to e38cb41.", + "timestamp": "2011-12-12T14:27:31+02:00", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "author": { + "name": "Jordi Mallach", + "email": "jordi@softcatala.org" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + }, + { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + } + ], + "total_commits_count": 4 +}` + var event *PushEvent + err := json.Unmarshal([]byte(jsonObject), &event) + + if err != nil { + t.Errorf("Push Event can not unmarshaled: %v\n ", err.Error()) + } + + if event == nil { + t.Errorf("Push Event is null") + } + + if event.ProjectID != 15 { + t.Errorf("ProjectID is %v, want %v", event.ProjectID, 15) + } + + if event.UserName != "John Smith" { + t.Errorf("Username is %s, want %s", event.UserName, "John Smith") + } + + if event.Commits[0] == nil || event.Commits[0].Timestamp == nil { + t.Errorf("Commit Timestamp isn't nil") + } + + if event.Commits[0] == nil || event.Commits[0].Author.Name != "Jordi Mallach" { + t.Errorf("Commit Username is %s, want %s", event.UserName, "Jordi Mallach") + } +} + +func TestMergeEventUnmarshal(t *testing.T) { + + jsonObject := `{ + "object_kind": "merge_request", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "object_attributes": { + "id": 99, + "target_branch": "master", + "source_branch": "ms-viewport", + "source_project_id": 14, + "author_id": 51, + "assignee_id": 6, + "title": "MS-Viewport", + "created_at": "2013-12-03T17:23:34Z", + "updated_at": "2013-12-03T17:23:34Z", + "st_commits": null, + "st_diffs": null, + "milestone_id": null, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 14, + "iid": 1, + "description": "", + "source":{ + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility":"private", + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" + }, + "target": { + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility":"private", + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" + }, + "last_commit": { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + } + }, + "work_in_progress": false, + "url": "http://example.com/diaspora/merge_requests/1", + "action": "open", + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } + } +}` + + var event *MergeEvent + err := json.Unmarshal([]byte(jsonObject), &event) + + if err != nil { + t.Errorf("Merge Event can not unmarshaled: %v\n ", err.Error()) + } + + if event == nil { + t.Errorf("Merge Event is null") + } + + if event.ObjectAttributes.ID != 99 { + t.Errorf("ObjectAttributes.ID is %v, want %v", event.ObjectAttributes.ID, 99) + } + + if event.ObjectAttributes.Source.Homepage != "http://example.com/awesome_space/awesome_project" { + t.Errorf("ObjectAttributes.Source.Homepage is %v, want %v", event.ObjectAttributes.Source.Homepage, "http://example.com/awesome_space/awesome_project") + } + + if event.ObjectAttributes.LastCommit.ID != "da1560886d4f094c3e6c9ef40349f7d38b5d27d7" { + t.Errorf("ObjectAttributes.LastCommit.ID is %v, want %s", event.ObjectAttributes.LastCommit.ID, "da1560886d4f094c3e6c9ef40349f7d38b5d27d7") + } + if event.ObjectAttributes.Assignee.Name != "User1" { + t.Errorf("Assignee.Name is %v, want %v", event.ObjectAttributes.ID, "User1") + } + + if event.ObjectAttributes.Assignee.Username != "user1" { + t.Errorf("ObjectAttributes is %v, want %v", event.ObjectAttributes.Assignee.Username, "user1") + } + + if event.User.Name == "" { + t.Errorf("Username is %s, want %s", event.User.Name, "Administrator") + } + + if event.ObjectAttributes.LastCommit.Timestamp == nil { + t.Errorf("Timestamp isn't nil") + } + + if name := event.ObjectAttributes.LastCommit.Author.Name; name != "GitLab dev user" { + t.Errorf("Commit Username is %s, want %s", name, "GitLab dev user") + } +} + +func TestPipelineEventUnmarshal(t *testing.T) { + jsonObject := `{ + "object_kind": "pipeline", + "object_attributes":{ + "id": 31, + "ref": "master", + "tag": false, + "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "status": "success", + "stages":[ + "build", + "test", + "deploy" + ], + "created_at": "2016-08-12 15:23:28 UTC", + "finished_at": "2016-08-12 15:26:29 UTC", + "duration": 63 + }, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "project":{ + "name": "Gitlab Test", + "description": "Atque in sunt eos similique dolores voluptatem.", + "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test", + "avatar_url": null, + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility": "private", + "path_with_namespace": "gitlab-org/gitlab-test", + "default_branch": "master" + }, + "commit":{ + "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "message": "test\n", + "timestamp": "2016-08-12T17:23:21+02:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "author":{ + "name": "User", + "email": "user@gitlab.com" + } + }, + "builds":[ + { + "id": 380, + "stage": "deploy", + "name": "production", + "status": "skipped", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": null, + "finished_at": null, + "when": "manual", + "manual": true, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 377, + "stage": "test", + "name": "test-image", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:26:12 UTC", + "finished_at": null, + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": { + "id": 6, + "description": "Kubernetes Runner", + "active": true, + "is_shared": true + }, + "artifacts_file":{ + "filename": "artifacts.zip", + "size": 1319148 + } + }, + { + "id": 378, + "stage": "test", + "name": "test-build", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:26:12 UTC", + "finished_at": "2016-08-12 15:26:29 UTC", + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 376, + "stage": "build", + "name": "build-image", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:24:56 UTC", + "finished_at": "2016-08-12 15:25:26 UTC", + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 379, + "stage": "deploy", + "name": "staging", + "status": "created", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": null, + "finished_at": null, + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + } + ] +}` + + var event *PipelineEvent + err := json.Unmarshal([]byte(jsonObject), &event) + + if err != nil { + t.Errorf("Pipeline Event can not unmarshaled: %v\n ", err.Error()) + } + + if event == nil { + t.Errorf("Pipeline Event is null") + } + + if event.ObjectAttributes.ID != 31 { + t.Errorf("ObjectAttributes is %v, want %v", event.ObjectAttributes.ID, 1977) + } + + if event.User.Name == "" { + t.Errorf("Username is %s, want %s", event.User.Name, "Administrator") + } + + if event.Commit.Timestamp == nil { + t.Errorf("Timestamp isn't nil") + } + + if name := event.Commit.Author.Name; name != "User" { + t.Errorf("Commit Username is %s, want %s", name, "User") + } +} + +func TestBuildEventUnmarshal(t *testing.T) { + jsonObject := `{ + "object_kind": "build", + "ref": "gitlab-script-trigger", + "tag": false, + "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "build_id": 1977, + "build_name": "test", + "build_stage": "test", + "build_status": "created", + "build_started_at": null, + "build_finished_at": null, + "build_duration": 23.265997, + "build_allow_failure": false, + "project_id": 380, + "project_name": "gitlab-org/gitlab-test", + "user": { + "id": 3, + "name": "User", + "email": "user@gitlab.com" + }, + "commit": { + "id": 2366, + "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "message": "test\n", + "author_name": "User", + "author_email": "user@gitlab.com", + "status": "created", + "duration": 199, + "started_at": null, + "finished_at": null + }, + "repository": { + "name": "gitlab_test", + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "description": "Atque in sunt eos similique dolores voluptatem.", + "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test", + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", + "visibility": "private" + } +}` + var event *BuildEvent + err := json.Unmarshal([]byte(jsonObject), &event) + + if err != nil { + t.Errorf("Build Event can not unmarshaled: %v\n ", err.Error()) + } + + if event == nil { + t.Errorf("Build Event is null") + } + + if event.BuildID != 1977 { + t.Errorf("BuildID is %v, want %v", event.BuildID, 1977) + } +} + +func TestMergeEventUnmarshalFromGroup(t *testing.T) { + + jsonObject := `{ + "object_kind": "merge_request", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/d22738dc40839e3d95fca77ca3eac067?s=80\u0026d=identicon" + }, + "project": { + "name": "example-project", + "description": "", + "web_url": "http://example.com/exm-namespace/example-project", + "avatar_url": null, + "git_ssh_url": "git@example.com:exm-namespace/example-project.git", + "git_http_url": "http://example.com/exm-namespace/example-project.git", + "namespace": "exm-namespace", + "visibility": "public", + "path_with_namespace": "exm-namespace/example-project", + "default_branch": "master", + "homepage": "http://example.com/exm-namespace/example-project", + "url": "git@example.com:exm-namespace/example-project.git", + "ssh_url": "git@example.com:exm-namespace/example-project.git", + "http_url": "http://example.com/exm-namespace/example-project.git" + }, + "object_attributes": { + "id": 15917, + "target_branch ": "master ", + "source_branch ": "source-branch-test ", + "source_project_id ": 87, + "author_id ": 15, + "assignee_id ": 29, + "title ": "source-branch-test ", + "created_at ": "2016 - 12 - 01 13: 11: 10 UTC ", + "updated_at ": "2016 - 12 - 01 13: 21: 20 UTC ", + "milestone_id ": null, + "state ": "merged ", + "merge_status ": "can_be_merged ", + "target_project_id ": 87, + "iid ": 1402, + "description ": "word doc support for e - ticket ", + "position ": 0, + "locked_at ": null, + "updated_by_id ": null, + "merge_error ": null, + "merge_params": { + "force_remove_source_branch": "0" + }, + "merge_when_build_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": "ac3ca1559bc39abf963586372eff7f8fdded646e", + "deleted_at": null, + "approvals_before_merge": null, + "rebase_commit_sha": null, + "in_progress_merge_commit_sha": null, + "lock_version": 0, + "time_estimate": 0, + "source": { + "name": "example-project", + "description": "", + "web_url": "http://example.com/exm-namespace/example-project", + "avatar_url": null, + "git_ssh_url": "git@example.com:exm-namespace/example-project.git", + "git_http_url": "http://example.com/exm-namespace/example-project.git", + "namespace": "exm-namespace", + "visibility": "public", + "path_with_namespace": "exm-namespace/example-project", + "default_branch": "master", + "homepage": "http://example.com/exm-namespace/example-project", + "url": "git@example.com:exm-namespace/example-project.git", + "ssh_url": "git@example.com:exm-namespace/example-project.git", + "http_url": "http://example.com/exm-namespace/example-project.git" + }, + "target": { + "name": "example-project", + "description": "", + "web_url": "http://example.com/exm-namespace/example-project", + "avatar_url": null, + "git_ssh_url": "git@example.com:exm-namespace/example-project.git", + "git_http_url": "http://example.com/exm-namespace/example-project.git", + "namespace": "exm-namespace", + "visibility": "public", + "path_with_namespace": "exm-namespace/example-project", + "default_branch": "master", + "homepage": "http://example.com/exm-namespace/example-project", + "url": "git@example.com:exm-namespace/example-project.git", + "ssh_url": "git@example.com:exm-namespace/example-project.git", + "http_url": "http://example.com/exm-namespace/example-project.git" + }, + "last_commit": { + "id": "61b6a0d35dbaf915760233b637622e383d3cc9ec", + "message": "commit message", + "timestamp": "2016-12-01T15:07:53+02:00", + "url": "http://example.com/exm-namespace/example-project/commit/61b6a0d35dbaf915760233b637622e383d3cc9ec", + "author": { + "name": "Test User", + "email": "test.user@mail.com" + } + }, + "work_in_progress": false, + "url": "http://example.com/exm-namespace/example-project/merge_requests/1402", + "action": "merge" + }, + "repository": { + "name": "example-project", + "url": "git@example.com:exm-namespace/example-project.git", + "description": "", + "homepage": "http://example.com/exm-namespace/example-project" + }, + "assignee": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/d22738dc40839e3d95fca77ca3eac067?s=80\u0026d=identicon" + } +}` + + var event *MergeEvent + err := json.Unmarshal([]byte(jsonObject), &event) + + if err != nil { + t.Errorf("Group Merge Event can not unmarshaled: %v\n ", err.Error()) + } + + if event == nil { + t.Errorf("Group Merge Event is null") + } + + if event.ObjectKind != "merge_request" { + t.Errorf("ObjectKind is %v, want %v", event.ObjectKind, "merge_request") + } + + if event.User.Username != "root" { + t.Errorf("User.Username is %v, want %v", event.User.Username, "root") + } + + if event.Project.Name != "example-project" { + t.Errorf("Project.Name is %v, want %v", event.Project.Name, "example-project") + } + + if event.ObjectAttributes.ID != 15917 { + t.Errorf("ObjectAttributes.ID is %v, want %v", event.ObjectAttributes.ID, 15917) + } + + if event.ObjectAttributes.Source.Name != "example-project" { + t.Errorf("ObjectAttributes.Source.Name is %v, want %v", event.ObjectAttributes.Source.Name, "example-project") + } + + if event.ObjectAttributes.LastCommit.Author.Email != "test.user@mail.com" { + t.Errorf("ObjectAttributes.LastCommit.Author.Email is %v, want %v", event.ObjectAttributes.LastCommit.Author.Email, "test.user@mail.com") + } + + if event.Repository.Name != "example-project" { + t.Errorf("Repository.Name is %v, want %v", event.Repository.Name, "example-project") + } + + if event.Assignee.Username != "root" { + t.Errorf("Assignee.Username is %v, want %v", event.Assignee, "root") + } + + if event.User.Name == "" { + t.Errorf("Username is %s, want %s", event.User.Name, "Administrator") + } + + if event.ObjectAttributes.LastCommit.Timestamp == nil { + t.Errorf("Timestamp isn't nil") + } + + if name := event.ObjectAttributes.LastCommit.Author.Name; name != "Test User" { + t.Errorf("Commit Username is %s, want %s", name, "Test User") + } +} diff --git a/api/events.go b/api/events.go new file mode 100644 index 0000000..ad25e17 --- /dev/null +++ b/api/events.go @@ -0,0 +1,147 @@ +// +// Copyright 2017, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// EventsService handles communication with the event related methods of +// the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/events.html +type EventsService struct { + client *Client +} + +// ContributionEvent represents a user's contribution +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/events.html#get-user-contribution-events +type ContributionEvent struct { + Title string `json:"title"` + ProjectID int `json:"project_id"` + ActionName string `json:"action_name"` + TargetID int `json:"target_id"` + TargetIID int `json:"target_iid"` + TargetType string `json:"target_type"` + AuthorID int `json:"author_id"` + TargetTitle string `json:"target_title"` + CreatedAt *time.Time `json:"created_at"` + PushData struct { + CommitCount int `json:"commit_count"` + Action string `json:"action"` + RefType string `json:"ref_type"` + CommitFrom string `json:"commit_from"` + CommitTo string `json:"commit_to"` + Ref string `json:"ref"` + CommitTitle string `json:"commit_title"` + } `json:"push_data"` + Note *Note `json:"note"` + Author struct { + Name string `json:"name"` + Username string `json:"username"` + ID int `json:"id"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + } `json:"author"` + AuthorUsername string `json:"author_username"` +} + +// ListContributionEventsOptions represents the options for GetUserContributionEvents +// +// GitLap API docs: +// https://docs.gitlab.com/ce/api/events.html#get-user-contribution-events +type ListContributionEventsOptions struct { + ListOptions + Action *EventTypeValue `url:"action,omitempty" json:"action,omitempty"` + TargetType *EventTargetTypeValue `url:"target_type,omitempty" json:"target_type,omitempty"` + Before *ISOTime `url:"before,omitempty" json:"before,omitempty"` + After *ISOTime `url:"after,omitempty" json:"after,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + +// ListUserContributionEvents retrieves user contribution events +// for the specified user, sorted from newest to oldest. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/events.html#get-user-contribution-events +func (s *UsersService) ListUserContributionEvents(uid interface{}, opt *ListContributionEventsOptions, options ...OptionFunc) ([]*ContributionEvent, *Response, error) { + user, err := parseID(uid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("users/%s/events", user) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var cs []*ContributionEvent + resp, err := s.client.Do(req, &cs) + if err != nil { + return nil, resp, err + } + + return cs, resp, err +} + +// ListCurrentUserContributionEvents gets a list currently authenticated user's events +// +// GitLab API docs: https://docs.gitlab.com/ce/api/events.html#list-currently-authenticated-user-39-s-events +func (s *EventsService) ListCurrentUserContributionEvents(opt *ListContributionEventsOptions, options ...OptionFunc) ([]*ContributionEvent, *Response, error) { + req, err := s.client.NewRequest("GET", "events", opt, options) + if err != nil { + return nil, nil, err + } + + var cs []*ContributionEvent + resp, err := s.client.Do(req, &cs) + if err != nil { + return nil, resp, err + } + + return cs, resp, err +} + +// ListProjectVisibleEvents gets a list of visible events for a particular project +// +// GitLab API docs: https://docs.gitlab.com/ee/api/events.html#list-a-project-s-visible-events +func (s *EventsService) ListProjectVisibleEvents(pid interface{}, opt *ListContributionEventsOptions, options ...OptionFunc) ([]*ContributionEvent, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/events", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var cs []*ContributionEvent + resp, err := s.client.Do(req, &cs) + if err != nil { + return nil, resp, err + } + + return cs, resp, err +} diff --git a/api/feature_flags.go b/api/feature_flags.go new file mode 100644 index 0000000..b6380ab --- /dev/null +++ b/api/feature_flags.go @@ -0,0 +1,79 @@ +package gitlab + +import ( + "fmt" + "net/url" +) + +// FeaturesService handles the communication with the application FeaturesService +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/features.html +type FeaturesService struct { + client *Client +} + +// Feature represents a GitLab feature flag. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/features.html +type Feature struct { + Name string `json:"name"` + State string `json:"state"` + Gates []Gate +} + +// Gate represents a gate of a GitLab feature flag. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/features.html +type Gate struct { + Key string `json:"key"` + Value interface{} `json:"value"` +} + +func (f Feature) String() string { + return Stringify(f) +} + +// ListFeatures gets a list of feature flags +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/features.html#list-all-features +func (s *FeaturesService) ListFeatures(options ...OptionFunc) ([]*Feature, *Response, error) { + req, err := s.client.NewRequest("GET", "features", nil, options) + if err != nil { + return nil, nil, err + } + + var f []*Feature + resp, err := s.client.Do(req, &f) + if err != nil { + return nil, resp, err + } + return f, resp, err +} + +// SetFeatureFlag sets or creates a feature flag gate +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/features.html#set-or-create-a-feature +func (s *FeaturesService) SetFeatureFlag(name string, value interface{}, options ...OptionFunc) (*Feature, *Response, error) { + u := fmt.Sprintf("features/%s", url.QueryEscape(name)) + + opt := struct { + Value interface{} `url:"value" json:"value"` + }{ + value, + } + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + f := &Feature{} + resp, err := s.client.Do(req, f) + if err != nil { + return nil, resp, err + } + return f, resp, err +} diff --git a/api/feature_flags_test.go b/api/feature_flags_test.go new file mode 100644 index 0000000..89dfd24 --- /dev/null +++ b/api/feature_flags_test.go @@ -0,0 +1,92 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestListFeatureFlags(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/features", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, ` + [ + { + "name": "experimental_feature", + "state": "off", + "gates": [ + { + "key": "boolean", + "value": false + } + ] + }, + { + "name": "new_library", + "state": "on" + } + ] + `) + }) + + features, _, err := client.Features.ListFeatures() + if err != nil { + t.Errorf("Features.ListFeatures returned error: %v", err) + } + + want := []*Feature{ + {Name: "experimental_feature", State: "off", Gates: []Gate{ + {Key: "boolean", Value: false}, + }}, + {Name: "new_library", State: "on"}, + } + if !reflect.DeepEqual(want, features) { + t.Errorf("Features.ListFeatures returned %+v, want %+v", features, want) + } +} + +func TestSetFeatureFlag(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/features/new_library", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, ` + { + "name": "new_library", + "state": "conditional", + "gates": [ + { + "key": "boolean", + "value": false + }, + { + "key": "percentage_of_time", + "value": 30 + } + ] + } + `) + }) + + feature, _, err := client.Features.SetFeatureFlag("new_library", "30") + if err != nil { + t.Errorf("Features.SetFeatureFlag returned error: %v", err) + } + + want := &Feature{ + Name: "new_library", + State: "conditional", + Gates: []Gate{ + {Key: "boolean", Value: false}, + {Key: "percentage_of_time", Value: 30.0}, + }, + } + if !reflect.DeepEqual(want, feature) { + t.Errorf("Features.SetFeatureFlag returned %+v, want %+v", feature, want) + } +} diff --git a/api/gitignore_templates.go b/api/gitignore_templates.go new file mode 100644 index 0000000..5c911f4 --- /dev/null +++ b/api/gitignore_templates.go @@ -0,0 +1,84 @@ +// +// Copyright 2018, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// GitIgnoreTemplatesService handles communication with the gitignore +// templates related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/templates/gitignores.html +type GitIgnoreTemplatesService struct { + client *Client +} + +// GitIgnoreTemplate represents a GitLab gitignore template. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/templates/gitignores.html +type GitIgnoreTemplate struct { + Name string `json:"name"` + Content string `json:"content"` +} + +// ListTemplatesOptions represents the available ListAllTemplates() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/gitignores.html#list-gitignore-templates +type ListTemplatesOptions ListOptions + +// ListTemplates get a list of available git ignore templates +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/gitignores.html#list-gitignore-templates +func (s *GitIgnoreTemplatesService) ListTemplates(opt *ListTemplatesOptions, options ...OptionFunc) ([]*GitIgnoreTemplate, *Response, error) { + req, err := s.client.NewRequest("GET", "templates/gitignores", opt, options) + if err != nil { + return nil, nil, err + } + + var gs []*GitIgnoreTemplate + resp, err := s.client.Do(req, &gs) + if err != nil { + return nil, resp, err + } + + return gs, resp, err +} + +// GetTemplate get a git ignore template +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/gitignores.html#single-gitignore-template +func (s *GitIgnoreTemplatesService) GetTemplate(key string, options ...OptionFunc) (*GitIgnoreTemplate, *Response, error) { + u := fmt.Sprintf("templates/gitignores/%s", url.QueryEscape(key)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + g := new(GitIgnoreTemplate) + resp, err := s.client.Do(req, g) + if err != nil { + return nil, resp, err + } + + return g, resp, err +} diff --git a/api/gitlab.go b/api/gitlab.go index d2450fb..29a7b12 100644 --- a/api/gitlab.go +++ b/api/gitlab.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package gitlab import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -25,62 +26,241 @@ import ( "io/ioutil" "net/http" "net/url" + "sort" "strconv" "strings" + "time" "github.com/google/go-querystring/query" + "golang.org/x/oauth2" ) const ( - libraryVersion = "0.1" - defaultBaseURL = "https://gitlab.com/api/v3/" - userAgent = "go-gitlab/" + libraryVersion + defaultBaseURL = "https://gitlab.com/" + apiVersionPath = "api/v4/" + userAgent = "go-gitlab" ) -// AccessLevel represents a permission level within GitLab. +// authType represents an authentication type within GitLab. // -// GitLab API docs: http://doc.gitlab.com/ce/permissions/permissions.html -type AccessLevel int +// GitLab API docs: https://docs.gitlab.com/ce/api/ +type authType int -// List of available access levels +// List of available authentication types. // -// GitLab API docs: http://doc.gitlab.com/ce/permissions/permissions.html +// GitLab API docs: https://docs.gitlab.com/ce/api/ const ( - GuestPermissions AccessLevel = 10 - ReporterPermissions AccessLevel = 20 - DeveloperPermissions AccessLevel = 30 - MasterPermissions AccessLevel = 40 - OwnerPermission AccessLevel = 50 + basicAuth authType = iota + oAuthToken + privateToken ) -// NotificationLevel represents a notification level within Gitlab. +// AccessLevelValue represents a permission level within GitLab. // -// GitLab API docs: http://doc.gitlab.com/ce/...? -type NotificationLevel int +// GitLab API docs: https://docs.gitlab.com/ce/permissions/permissions.html +type AccessLevelValue int -// List of available notification levels +// List of available access levels // -// GitLab API docs: http://doc.gitlab.com/ce/...? +// GitLab API docs: https://docs.gitlab.com/ce/permissions/permissions.html const ( - DisabledNotifications NotificationLevel = iota - ParticipatingNotifications - WatchNotifications - GlobalNotifications - MentionNotifications + NoPermissions AccessLevelValue = 0 + GuestPermissions AccessLevelValue = 10 + ReporterPermissions AccessLevelValue = 20 + DeveloperPermissions AccessLevelValue = 30 + MaintainerPermissions AccessLevelValue = 40 + OwnerPermissions AccessLevelValue = 50 + + // These are deprecated and should be removed in a future version + MasterPermissions AccessLevelValue = 40 + OwnerPermission AccessLevelValue = 50 ) -// VisibilityLevel represents a visibility level within GitLab. +// BuildStateValue represents a GitLab build state. +type BuildStateValue string + +// These constants represent all valid build states. +const ( + Pending BuildStateValue = "pending" + Running BuildStateValue = "running" + Success BuildStateValue = "success" + Failed BuildStateValue = "failed" + Canceled BuildStateValue = "canceled" + Skipped BuildStateValue = "skipped" +) + +// ISOTime represents an ISO 8601 formatted date +type ISOTime time.Time + +// ISO 8601 date format +const iso8601 = "2006-01-02" + +// MarshalJSON implements the json.Marshaler interface +func (t ISOTime) MarshalJSON() ([]byte, error) { + if y := time.Time(t).Year(); y < 0 || y >= 10000 { + // ISO 8901 uses 4 digits for the years + return nil, errors.New("ISOTime.MarshalJSON: year outside of range [0,9999]") + } + + b := make([]byte, 0, len(iso8601)+2) + b = append(b, '"') + b = time.Time(t).AppendFormat(b, iso8601) + b = append(b, '"') + + return b, nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface +func (t *ISOTime) UnmarshalJSON(data []byte) error { + // Ignore null, like in the main JSON package + if string(data) == "null" { + return nil + } + + isotime, err := time.Parse(`"`+iso8601+`"`, string(data)) + *t = ISOTime(isotime) + + return err +} + +// EncodeValues implements the query.Encoder interface +func (t *ISOTime) EncodeValues(key string, v *url.Values) error { + if t == nil || (time.Time(*t)).IsZero() { + return nil + } + v.Add(key, t.String()) + return nil +} + +// String implements the Stringer interface +func (t ISOTime) String() string { + return time.Time(t).Format(iso8601) +} + +// NotificationLevelValue represents a notification level. +type NotificationLevelValue int + +// String implements the fmt.Stringer interface. +func (l NotificationLevelValue) String() string { + return notificationLevelNames[l] +} + +// MarshalJSON implements the json.Marshaler interface. +func (l NotificationLevelValue) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (l *NotificationLevelValue) UnmarshalJSON(data []byte) error { + var raw interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + switch raw := raw.(type) { + case float64: + *l = NotificationLevelValue(raw) + case string: + *l = notificationLevelTypes[raw] + case nil: + // No action needed. + default: + return fmt.Errorf("json: cannot unmarshal %T into Go value of type %T", raw, *l) + } + + return nil +} + +// List of valid notification levels. +const ( + DisabledNotificationLevel NotificationLevelValue = iota + ParticipatingNotificationLevel + WatchNotificationLevel + GlobalNotificationLevel + MentionNotificationLevel + CustomNotificationLevel +) + +var notificationLevelNames = [...]string{ + "disabled", + "participating", + "watch", + "global", + "mention", + "custom", +} + +var notificationLevelTypes = map[string]NotificationLevelValue{ + "disabled": DisabledNotificationLevel, + "participating": ParticipatingNotificationLevel, + "watch": WatchNotificationLevel, + "global": GlobalNotificationLevel, + "mention": MentionNotificationLevel, + "custom": CustomNotificationLevel, +} + +// VisibilityValue represents a visibility level within GitLab. // -// GitLab API docs: http://doc.gitlab.com/ce/...? -type VisibilityLevel int +// GitLab API docs: https://docs.gitlab.com/ce/api/ +type VisibilityValue string // List of available visibility levels // -// GitLab API docs: http://doc.gitlab.com/ce/...? +// GitLab API docs: https://docs.gitlab.com/ce/api/ const ( - PrivateVisibility VisibilityLevel = 0 - InternalVisibility VisibilityLevel = 10 - PublicVisibility VisibilityLevel = 20 + PrivateVisibility VisibilityValue = "private" + InternalVisibility VisibilityValue = "internal" + PublicVisibility VisibilityValue = "public" +) + +// MergeMethodValue represents a project merge type within GitLab. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#project-merge-method +type MergeMethodValue string + +// List of available merge type +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#project-merge-method +const ( + NoFastForwardMerge MergeMethodValue = "merge" + FastForwardMerge MergeMethodValue = "ff" + RebaseMerge MergeMethodValue = "rebase_merge" +) + +// EventTypeValue represents actions type for contribution events +type EventTypeValue string + +// List of available action type +// +// GitLab API docs: https://docs.gitlab.com/ce/api/events.html#action-types +const ( + CreatedEventType EventTypeValue = "created" + UpdatedEventType EventTypeValue = "updated" + ClosedEventType EventTypeValue = "closed" + ReopenedEventType EventTypeValue = "reopened" + PushedEventType EventTypeValue = "pushed" + CommentedEventType EventTypeValue = "commented" + MergedEventType EventTypeValue = "merged" + JoinedEventType EventTypeValue = "joined" + LeftEventType EventTypeValue = "left" + DestroyedEventType EventTypeValue = "destroyed" + ExpiredEventType EventTypeValue = "expired" +) + +// EventTargetTypeValue represents actions type value for contribution events +type EventTargetTypeValue string + +// List of available action type +// +// GitLab API docs: https://docs.gitlab.com/ce/api/events.html#target-types +const ( + IssueEventTargetType EventTargetTypeValue = "issue" + MilestoneEventTargetType EventTargetTypeValue = "milestone" + MergeRequestEventTargetType EventTargetTypeValue = "merge_request" + NoteEventTargetType EventTargetTypeValue = "note" + ProjectEventTargetType EventTargetTypeValue = "project" + SnippetEventTargetType EventTargetTypeValue = "snippet" + UserEventTargetType EventTargetTypeValue = "user" ) // A Client manages communication with the GitLab API. @@ -89,36 +269,85 @@ type Client struct { client *http.Client // Base URL for API requests. Defaults to the public GitLab API, but can be - // set to a domain endpoint to use with aself hosted GitLab server. baseURL + // set to a domain endpoint to use with a self hosted GitLab server. baseURL // should always be specified with a trailing slash. baseURL *url.URL - // Private token used to make authenticated API calls. + // Token type used to make authenticated API calls. + authType authType + + // Username and password used for basix authentication. + username, password string + + // Token used to make authenticated API calls. token string // User agent used when communicating with the GitLab API. UserAgent string // Services used for talking to different parts of the GitLab API. - Branches *BranchesService - Commits *CommitsService - DeployKeys *DeployKeysService - Groups *GroupsService - Issues *IssuesService - Labels *LabelsService - MergeRequests *MergeRequestsService - Milestones *MilestonesService - Namespaces *NamespacesService - Notes *NotesService - Projects *ProjectsService - ProjectSnippets *ProjectSnippetsService - Repositories *RepositoriesService - RepositoryFiles *RepositoryFilesService - Services *ServicesService - Session *SessionService - Settings *SettingsService - SystemHooks *SystemHooksService - Users *UsersService + AccessRequests *AccessRequestsService + AwardEmoji *AwardEmojiService + Boards *IssueBoardsService + Branches *BranchesService + BroadcastMessage *BroadcastMessagesService + BuildVariables *BuildVariablesService + CIYMLTemplate *CIYMLTemplatesService + Commits *CommitsService + CustomAttribute *CustomAttributesService + DeployKeys *DeployKeysService + Deployments *DeploymentsService + Discussions *DiscussionsService + Environments *EnvironmentsService + Events *EventsService + Features *FeaturesService + GitIgnoreTemplates *GitIgnoreTemplatesService + GroupBadges *GroupBadgesService + GroupIssueBoards *GroupIssueBoardsService + GroupMembers *GroupMembersService + GroupMilestones *GroupMilestonesService + GroupVariables *GroupVariablesService + Groups *GroupsService + IssueLinks *IssueLinksService + Issues *IssuesService + Jobs *JobsService + Keys *KeysService + Labels *LabelsService + License *LicenseService + LicenseTemplates *LicenseTemplatesService + MergeRequestApprovals *MergeRequestApprovalsService + MergeRequests *MergeRequestsService + Milestones *MilestonesService + Namespaces *NamespacesService + Notes *NotesService + NotificationSettings *NotificationSettingsService + PagesDomains *PagesDomainsService + PipelineSchedules *PipelineSchedulesService + PipelineTriggers *PipelineTriggersService + Pipelines *PipelinesService + ProjectBadges *ProjectBadgesService + ProjectCluster *ProjectClustersService + ProjectMembers *ProjectMembersService + ProjectSnippets *ProjectSnippetsService + ProjectVariables *ProjectVariablesService + Projects *ProjectsService + ProtectedBranches *ProtectedBranchesService + ProtectedTags *ProtectedTagsService + Repositories *RepositoriesService + RepositoryFiles *RepositoryFilesService + Runners *RunnersService + Search *SearchService + Services *ServicesService + Settings *SettingsService + Sidekiq *SidekiqService + Snippets *SnippetsService + SystemHooks *SystemHooksService + Tags *TagsService + Todos *TodosService + Users *UsersService + Validate *ValidateService + Version *VersionService + Wikis *WikisService } // ListOptions specifies the optional parameters to various List methods that @@ -133,37 +362,135 @@ type ListOptions struct { // NewClient returns a new GitLab API client. If a nil httpClient is // provided, http.DefaultClient will be used. To use API methods which require -// authentication, provide a valid private token. +// authentication, provide a valid private or personal token. func NewClient(httpClient *http.Client, token string) *Client { + client := newClient(httpClient) + client.authType = privateToken + client.token = token + return client +} + +// NewBasicAuthClient returns a new GitLab API client. If a nil httpClient is +// provided, http.DefaultClient will be used. To use API methods which require +// authentication, provide a valid username and password. +func NewBasicAuthClient(httpClient *http.Client, endpoint, username, password string) (*Client, error) { + client := newClient(httpClient) + client.authType = basicAuth + client.username = username + client.password = password + client.SetBaseURL(endpoint) + + err := client.requestOAuthToken(context.TODO()) + if err != nil { + return nil, err + } + + return client, nil +} + +func (c *Client) requestOAuthToken(ctx context.Context) error { + config := &oauth2.Config{ + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("%s://%s/oauth/authorize", c.BaseURL().Scheme, c.BaseURL().Host), + TokenURL: fmt.Sprintf("%s://%s/oauth/token", c.BaseURL().Scheme, c.BaseURL().Host), + }, + } + ctx = context.WithValue(ctx, oauth2.HTTPClient, c.client) + t, err := config.PasswordCredentialsToken(ctx, c.username, c.password) + if err != nil { + return err + } + c.token = t.AccessToken + return nil +} + +// NewOAuthClient returns a new GitLab API client. If a nil httpClient is +// provided, http.DefaultClient will be used. To use API methods which require +// authentication, provide a valid oauth token. +func NewOAuthClient(httpClient *http.Client, token string) *Client { + client := newClient(httpClient) + client.authType = oAuthToken + client.token = token + return client +} + +func newClient(httpClient *http.Client) *Client { if httpClient == nil { httpClient = http.DefaultClient } - c := &Client{client: httpClient, token: token, UserAgent: userAgent} + c := &Client{client: httpClient, UserAgent: userAgent} if err := c.SetBaseURL(defaultBaseURL); err != nil { - // should never happen since defaultBaseURL is our constant + // Should never happen since defaultBaseURL is our constant. panic(err) } + // Create the internal timeStats service. + timeStats := &timeStatsService{client: c} + + // Create all the public services. + c.AccessRequests = &AccessRequestsService{client: c} + c.AwardEmoji = &AwardEmojiService{client: c} + c.Boards = &IssueBoardsService{client: c} c.Branches = &BranchesService{client: c} + c.BroadcastMessage = &BroadcastMessagesService{client: c} + c.BuildVariables = &BuildVariablesService{client: c} + c.CIYMLTemplate = &CIYMLTemplatesService{client: c} c.Commits = &CommitsService{client: c} + c.CustomAttribute = &CustomAttributesService{client: c} c.DeployKeys = &DeployKeysService{client: c} + c.Deployments = &DeploymentsService{client: c} + c.Discussions = &DiscussionsService{client: c} + c.Environments = &EnvironmentsService{client: c} + c.Events = &EventsService{client: c} + c.Features = &FeaturesService{client: c} + c.GitIgnoreTemplates = &GitIgnoreTemplatesService{client: c} + c.GroupBadges = &GroupBadgesService{client: c} + c.GroupIssueBoards = &GroupIssueBoardsService{client: c} + c.GroupMembers = &GroupMembersService{client: c} + c.GroupMilestones = &GroupMilestonesService{client: c} + c.GroupVariables = &GroupVariablesService{client: c} c.Groups = &GroupsService{client: c} - c.Issues = &IssuesService{client: c} + c.IssueLinks = &IssueLinksService{client: c} + c.Issues = &IssuesService{client: c, timeStats: timeStats} + c.Jobs = &JobsService{client: c} + c.Keys = &KeysService{client: c} c.Labels = &LabelsService{client: c} - c.MergeRequests = &MergeRequestsService{client: c} + c.License = &LicenseService{client: c} + c.LicenseTemplates = &LicenseTemplatesService{client: c} + c.MergeRequestApprovals = &MergeRequestApprovalsService{client: c} + c.MergeRequests = &MergeRequestsService{client: c, timeStats: timeStats} c.Milestones = &MilestonesService{client: c} - c.Notes = &NotesService{client: c} c.Namespaces = &NamespacesService{client: c} - c.Projects = &ProjectsService{client: c} + c.Notes = &NotesService{client: c} + c.NotificationSettings = &NotificationSettingsService{client: c} + c.PagesDomains = &PagesDomainsService{client: c} + c.PipelineSchedules = &PipelineSchedulesService{client: c} + c.PipelineTriggers = &PipelineTriggersService{client: c} + c.Pipelines = &PipelinesService{client: c} + c.ProjectBadges = &ProjectBadgesService{client: c} + c.ProjectCluster = &ProjectClustersService{client: c} + c.ProjectMembers = &ProjectMembersService{client: c} c.ProjectSnippets = &ProjectSnippetsService{client: c} + c.ProjectVariables = &ProjectVariablesService{client: c} + c.Projects = &ProjectsService{client: c} + c.ProtectedBranches = &ProtectedBranchesService{client: c} + c.ProtectedTags = &ProtectedTagsService{client: c} c.Repositories = &RepositoriesService{client: c} c.RepositoryFiles = &RepositoryFilesService{client: c} + c.Runners = &RunnersService{client: c} + c.Search = &SearchService{client: c} c.Services = &ServicesService{client: c} - c.Session = &SessionService{client: c} c.Settings = &SettingsService{client: c} + c.Sidekiq = &SidekiqService{client: c} + c.Snippets = &SnippetsService{client: c} c.SystemHooks = &SystemHooksService{client: c} + c.Tags = &TagsService{client: c} + c.Todos = &TodosService{client: c} c.Users = &UsersService{client: c} + c.Validate = &ValidateService{client: c} + c.Version = &VersionService{client: c} + c.Wikis = &WikisService{client: c} return c } @@ -182,12 +509,18 @@ func (c *Client) SetBaseURL(urlStr string) error { urlStr += "/" } - var err error - c.baseURL, err = url.Parse(urlStr) + baseURL, err := url.Parse(urlStr) if err != nil { return err } + if !strings.HasSuffix(baseURL.Path, apiVersionPath) { + baseURL.Path += apiVersionPath + } + + // Update the base URL of the client. + c.baseURL = baseURL + return nil } @@ -196,16 +529,24 @@ func (c *Client) SetBaseURL(urlStr string) error { // Relative URL paths should always be specified without a preceding slash. If // specified, the value pointed to by body is JSON encoded and included as the // request body. -func (c *Client) NewRequest(method, path string, opt interface{}) (*http.Request, error) { +func (c *Client) NewRequest(method, path string, opt interface{}, options []OptionFunc) (*http.Request, error) { u := *c.baseURL - // Set the encoded opaque data - u.Opaque = c.baseURL.Path + path - - q, err := query.Values(opt) + unescaped, err := url.PathUnescape(path) if err != nil { return nil, err } - u.RawQuery = q.Encode() + + // Set the encoded path data + u.RawPath = c.baseURL.Path + path + u.Path = c.baseURL.Path + unescaped + + if opt != nil { + q, err := query.Values(opt) + if err != nil { + return nil, err + } + u.RawQuery = q.Encode() + } req := &http.Request{ Method: method, @@ -217,6 +558,16 @@ func (c *Client) NewRequest(method, path string, opt interface{}) (*http.Request Host: u.Host, } + for _, fn := range options { + if fn == nil { + continue + } + + if err := fn(req); err != nil { + return nil, err + } + } + if method == "POST" || method == "PUT" { bodyBytes, err := json.Marshal(opt) if err != nil { @@ -226,12 +577,22 @@ func (c *Client) NewRequest(method, path string, opt interface{}) (*http.Request u.RawQuery = "" req.Body = ioutil.NopCloser(bodyReader) + req.GetBody = func() (io.ReadCloser, error) { + return ioutil.NopCloser(bodyReader), nil + } req.ContentLength = int64(bodyReader.Len()) req.Header.Set("Content-Type", "application/json") } req.Header.Set("Accept", "application/json") - req.Header.Set("PRIVATE-TOKEN", c.token) + + switch c.authType { + case basicAuth, oAuthToken: + req.Header.Set("Authorization", "Bearer "+c.token) + case privateToken: + req.Header.Set("PRIVATE-TOKEN", c.token) + } + if c.UserAgent != "" { req.Header.Set("User-Agent", c.UserAgent) } @@ -246,64 +607,53 @@ type Response struct { *http.Response // These fields provide the page values for paginating through a set of - // results. Any or all of these may be set to the zero value for + // results. Any or all of these may be set to the zero value for // responses that are not part of a paginated set, or for which there // are no additional pages. - - NextPage int - PrevPage int - FirstPage int - LastPage int + TotalItems int + TotalPages int + ItemsPerPage int + CurrentPage int + NextPage int + PreviousPage int } -// newResponse creats a new Response for the provided http.Response. +// newResponse creates a new Response for the provided http.Response. func newResponse(r *http.Response) *Response { response := &Response{Response: r} response.populatePageValues() return response } +const ( + xTotal = "X-Total" + xTotalPages = "X-Total-Pages" + xPerPage = "X-Per-Page" + xPage = "X-Page" + xNextPage = "X-Next-Page" + xPrevPage = "X-Prev-Page" +) + // populatePageValues parses the HTTP Link response headers and populates the -// various pagination link values in the Reponse. +// various pagination link values in the Response. func (r *Response) populatePageValues() { - if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 { - for _, link := range strings.Split(links[0], ",") { - segments := strings.Split(strings.TrimSpace(link), ";") - - // link must at least have href and rel - if len(segments) < 2 { - continue - } - - // ensure href is properly formatted - if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") { - continue - } - - // try to pull out page parameter - url, err := url.Parse(segments[0][1 : len(segments[0])-1]) - if err != nil { - continue - } - page := url.Query().Get("page") - if page == "" { - continue - } - - for _, segment := range segments[1:] { - switch strings.TrimSpace(segment) { - case `rel="next"`: - r.NextPage, _ = strconv.Atoi(page) - case `rel="prev"`: - r.PrevPage, _ = strconv.Atoi(page) - case `rel="first"`: - r.FirstPage, _ = strconv.Atoi(page) - case `rel="last"`: - r.LastPage, _ = strconv.Atoi(page) - } - - } - } + if totalItems := r.Response.Header.Get(xTotal); totalItems != "" { + r.TotalItems, _ = strconv.Atoi(totalItems) + } + if totalPages := r.Response.Header.Get(xTotalPages); totalPages != "" { + r.TotalPages, _ = strconv.Atoi(totalPages) + } + if itemsPerPage := r.Response.Header.Get(xPerPage); itemsPerPage != "" { + r.ItemsPerPage, _ = strconv.Atoi(itemsPerPage) + } + if currentPage := r.Response.Header.Get(xPage); currentPage != "" { + r.CurrentPage, _ = strconv.Atoi(currentPage) + } + if nextPage := r.Response.Header.Get(xNextPage); nextPage != "" { + r.NextPage, _ = strconv.Atoi(nextPage) + } + if previousPage := r.Response.Header.Get(xPrevPage); previousPage != "" { + r.PreviousPage, _ = strconv.Atoi(previousPage) } } @@ -317,9 +667,16 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { if err != nil { return nil, err } - defer resp.Body.Close() + if resp.StatusCode == http.StatusUnauthorized && c.authType == basicAuth { + err = c.requestOAuthToken(req.Context()) + if err != nil { + return nil, err + } + return c.Do(req, v) + } + response := newResponse(resp) err = CheckResponse(resp) @@ -336,6 +693,7 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { err = json.NewDecoder(resp.Body).Decode(v) } } + return response, err } @@ -348,70 +706,118 @@ func parseID(id interface{}) (string, error) { case string: return v, nil default: - return "", errors.New("the ID must be an int or a string") + return "", fmt.Errorf("invalid ID type %#v, the ID must be an int or a string", id) } } // An ErrorResponse reports one or more errors caused by an API request. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/README.html#data-validation-and-error-reporting +// https://docs.gitlab.com/ce/api/README.html#data-validation-and-error-reporting type ErrorResponse struct { - Response *http.Response // HTTP response that caused this error - Message string `json:"message"` // error message - Errors []Error `json:"errors"` // more detail on individual errors -} - -func (r *ErrorResponse) Error() string { - path, _ := url.QueryUnescape(r.Response.Request.URL.Opaque) - ru := fmt.Sprintf("%s://%s%s", r.Response.Request.URL.Scheme, r.Response.Request.URL.Host, path) - - return fmt.Sprintf("%v %s: %d %v %+v", - r.Response.Request.Method, ru, r.Response.StatusCode, r.Message, r.Errors) -} - -// An Error reports more details on an individual error in an ErrorResponse. -// These are the possible validation error codes: -// -// missing: -// resource does not exist -// missing_field: -// a required field on a resource has not been set -// invalid: -// the formatting of a field is invalid -// already_exists: -// another resource has the same valid as this field -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/README.html#data-validation-and-error-reporting -type Error struct { - Resource string `json:"resource"` // resource on which the error occurred - Field string `json:"field"` // field on which the error occurred - Code string `json:"code"` // validation error code + Body []byte + Response *http.Response + Message string } -func (e *Error) Error() string { - return fmt.Sprintf("%v error caused by %v field on %v resource", - e.Code, e.Field, e.Resource) +func (e *ErrorResponse) Error() string { + path, _ := url.QueryUnescape(e.Response.Request.URL.Path) + u := fmt.Sprintf("%s://%s%s", e.Response.Request.URL.Scheme, e.Response.Request.URL.Host, path) + return fmt.Sprintf("%s %s: %d %s", e.Response.Request.Method, u, e.Response.StatusCode, e.Message) } -// CheckResponse checks the API response for errors, and returns them if -// present. A response is considered an error if it has a status code outside -// the 200 range. API error responses are expected to have either no response -// body, or a JSON response body that maps to ErrorResponse. Any other -// response body will be silently ignored. +// CheckResponse checks the API response for errors, and returns them if present. func CheckResponse(r *http.Response) error { - if c := r.StatusCode; 200 <= c && c <= 299 { + switch r.StatusCode { + case 200, 201, 202, 204, 304: return nil } + errorResponse := &ErrorResponse{Response: r} data, err := ioutil.ReadAll(r.Body) if err == nil && data != nil { - json.Unmarshal(data, errorResponse) + errorResponse.Body = data + + var raw interface{} + if err := json.Unmarshal(data, &raw); err != nil { + errorResponse.Message = "failed to parse unknown error format" + } else { + errorResponse.Message = parseError(raw) + } } + return errorResponse } +// Format: +// { +// "message": { +// "": [ +// "", +// "", +// ... +// ], +// "": { +// "": [ +// "", +// "", +// ... +// ], +// } +// }, +// "error": "" +// } +func parseError(raw interface{}) string { + switch raw := raw.(type) { + case string: + return raw + + case []interface{}: + var errs []string + for _, v := range raw { + errs = append(errs, parseError(v)) + } + return fmt.Sprintf("[%s]", strings.Join(errs, ", ")) + + case map[string]interface{}: + var errs []string + for k, v := range raw { + errs = append(errs, fmt.Sprintf("{%s: %s}", k, parseError(v))) + } + sort.Strings(errs) + return strings.Join(errs, ", ") + + default: + return fmt.Sprintf("failed to parse unexpected error type: %T", raw) + } +} + +// OptionFunc can be passed to all API requests to make the API call as if you were +// another user, provided your private token is from an administrator account. +// +// GitLab docs: https://docs.gitlab.com/ce/api/README.html#sudo +type OptionFunc func(*http.Request) error + +// WithSudo takes either a username or user ID and sets the SUDO request header +func WithSudo(uid interface{}) OptionFunc { + return func(req *http.Request) error { + user, err := parseID(uid) + if err != nil { + return err + } + req.Header.Set("SUDO", user) + return nil + } +} + +// WithContext runs the request with the provided context +func WithContext(ctx context.Context) OptionFunc { + return func(req *http.Request) error { + *req = *req.WithContext(ctx) + return nil + } +} + // Bool is a helper routine that allocates a new bool value // to store v and returns a pointer to it. func Bool(v bool) *bool { @@ -436,3 +842,64 @@ func String(v string) *string { *p = v return p } + +// AccessLevel is a helper routine that allocates a new AccessLevelValue +// to store v and returns a pointer to it. +func AccessLevel(v AccessLevelValue) *AccessLevelValue { + p := new(AccessLevelValue) + *p = v + return p +} + +// BuildState is a helper routine that allocates a new BuildStateValue +// to store v and returns a pointer to it. +func BuildState(v BuildStateValue) *BuildStateValue { + p := new(BuildStateValue) + *p = v + return p +} + +// NotificationLevel is a helper routine that allocates a new NotificationLevelValue +// to store v and returns a pointer to it. +func NotificationLevel(v NotificationLevelValue) *NotificationLevelValue { + p := new(NotificationLevelValue) + *p = v + return p +} + +// Visibility is a helper routine that allocates a new VisibilityValue +// to store v and returns a pointer to it. +func Visibility(v VisibilityValue) *VisibilityValue { + p := new(VisibilityValue) + *p = v + return p +} + +// MergeMethod is a helper routine that allocates a new MergeMethod +// to sotre v and returns a pointer to it. +func MergeMethod(v MergeMethodValue) *MergeMethodValue { + p := new(MergeMethodValue) + *p = v + return p +} + +// BoolValue is a boolean value with advanced json unmarshaling features. +type BoolValue bool + +// UnmarshalJSON allows 1 and 0 to be considered as boolean values +// Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/50122 +func (t *BoolValue) UnmarshalJSON(b []byte) error { + switch string(b) { + case `"1"`: + *t = true + return nil + case `"0"`: + *t = false + return nil + default: + var v bool + err := json.Unmarshal(b, &v) + *t = BoolValue(v) + return err + } +} diff --git a/api/gitlab_test.go b/api/gitlab_test.go index 983d828..dde0905 100644 --- a/api/gitlab_test.go +++ b/api/gitlab_test.go @@ -1,12 +1,12 @@ package gitlab import ( + "bytes" + "context" "encoding/json" "io/ioutil" "net/http" "net/http/httptest" - "net/url" - "reflect" "strings" "testing" ) @@ -33,7 +33,7 @@ func teardown(server *httptest.Server) { server.Close() } -func testUrl(t *testing.T, r *http.Request, want string) { +func testURL(t *testing.T, r *http.Request, want string) { if got := r.RequestURI; got != want { t.Errorf("Request url: %+v, want %s", got, want) } @@ -45,85 +45,135 @@ func testMethod(t *testing.T, r *http.Request, want string) { } } -type values map[string]string +func testBody(t *testing.T, r *http.Request, want string) { + buffer := new(bytes.Buffer) + _, err := buffer.ReadFrom(r.Body) -func testFormValues(t *testing.T, r *http.Request, values values) { - want := url.Values{} - for k, v := range values { - want.Add(k, v) + if err != nil { + t.Fatalf("Failed to Read Body: %v", err) } - r.ParseForm() - if got := r.Form; !reflect.DeepEqual(got, want) { - t.Errorf("Request parameters: %v, want %v", got, want) + if got := buffer.String(); got != want { + t.Errorf("Request body: %s, want %s", got, want) } } -func testHeader(t *testing.T, r *http.Request, header string, want string) { - if got := r.Header.Get(header); got != want { - t.Errorf("Header.Get(%q) returned %s, want %s", header, got, want) +func TestNewClient(t *testing.T) { + c := NewClient(nil, "") + expectedBaseURL := defaultBaseURL + apiVersionPath + + if c.BaseURL().String() != expectedBaseURL { + t.Errorf("NewClient BaseURL is %s, want %s", c.BaseURL().String(), expectedBaseURL) + } + if c.UserAgent != userAgent { + t.Errorf("NewClient UserAgent is %s, want %s", c.UserAgent, userAgent) } } -func testBody(t *testing.T, r *http.Request, want string) { - b, err := ioutil.ReadAll(r.Body) +func TestSetBaseURL(t *testing.T) { + expectedBaseURL := "http://gitlab.local/foo/" + apiVersionPath + c := NewClient(nil, "") + err := c.SetBaseURL("http://gitlab.local/foo") if err != nil { - t.Errorf("Error reading request body: %v", err) + t.Fatalf("Failed to SetBaseURL: %v", err) } - if got := string(b); got != want { - t.Errorf("request Body is %s, want %s", got, want) + if c.BaseURL().String() != expectedBaseURL { + t.Errorf("BaseURL is %s, want %s", c.BaseURL().String(), expectedBaseURL) } } -func testJsonBody(t *testing.T, r *http.Request, want values) { - b, err := ioutil.ReadAll(r.Body) +func TestCheckResponse(t *testing.T) { + req, err := NewClient(nil, "").NewRequest("GET", "test", nil, nil) if err != nil { - t.Errorf("Error reading request body: %v", err) + t.Fatalf("Failed to create request: %v", err) } - var got values - json.Unmarshal(b, &got) - - if !reflect.DeepEqual(got, want) { - t.Errorf("Request parameters: %v, want %v", got, want) + resp := &http.Response{ + Request: req, + StatusCode: http.StatusBadRequest, + Body: ioutil.NopCloser(strings.NewReader(` + { + "message": { + "prop1": [ + "message 1", + "message 2" + ], + "prop2":[ + "message 3" + ], + "embed1": { + "prop3": [ + "msg 1", + "msg2" + ] + }, + "embed2": { + "prop4": [ + "some msg" + ] + } + }, + "error": "message 1" + }`)), } -} -func responseBody(w http.ResponseWriter, filename string) { - body, _ := ioutil.ReadFile(filename) - w.Write([]byte(body)) -} + errResp := CheckResponse(resp) + if errResp == nil { + t.Fatal("Expected error response.") + } -func TestNewClient(t *testing.T) { - c := NewClient(nil, "") + want := "GET https://gitlab.com/api/v4/test: 400 {error: message 1}, {message: {embed1: {prop3: [msg 1, msg2]}}, {embed2: {prop4: [some msg]}}, {prop1: [message 1, message 2]}, {prop2: [message 3]}}" - if c.BaseURL().String() != defaultBaseURL { - t.Errorf("NewClient BaseURL is %s, want %s", c.BaseURL().String(), defaultBaseURL) - } - if c.UserAgent != userAgent { - t.Errorf("NewClient UserAgent is %s, want %s", c.UserAgent, userAgent) + if errResp.Error() != want { + t.Errorf("Expected error: %s, got %s", want, errResp.Error()) } } -func TestCheckResponse(t *testing.T) { - res := &http.Response{ - Request: &http.Request{}, - StatusCode: http.StatusBadRequest, - Body: ioutil.NopCloser(strings.NewReader(`{"message":"m", - "errors": [{"resource": "r", "field": "f", "code": "c"}]}`)), +func TestRequestWithContext(t *testing.T) { + ctx := context.WithValue(context.Background(), interface{}("myKey"), interface{}("myValue")) + req, err := NewClient(nil, "").NewRequest("GET", "test", nil, []OptionFunc{WithContext(ctx)}) + if err != nil { + t.Fatalf("Failed to create request: %v", err) } - err := CheckResponse(res).(*ErrorResponse) - if err == nil { - t.Errorf("Expected error response.") + if req.Context() != ctx { + t.Fatal("Context was not set correctly") } +} - want := &ErrorResponse{ - Response: res, - Message: "m", - Errors: []Error{{Resource: "r", Field: "f", Code: "c"}}, +func TestBoolValue(t *testing.T) { + testCases := map[string]struct { + data []byte + expected bool + }{ + "should unmarshal true as true": { + data: []byte("true"), + expected: true, + }, + "should unmarshal false as true": { + data: []byte("false"), + expected: false, + }, + "should unmarshal \"1\" as true": { + data: []byte(`"1"`), + expected: true, + }, + "should unmarshal \"0\" as false": { + data: []byte(`"0"`), + expected: false, + }, } - if !reflect.DeepEqual(err, want) { - t.Errorf("Error = %#v, want %#v", err, want) + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + var b BoolValue + if err := json.Unmarshal(testCase.data, &b); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if bool(b) != testCase.expected { + t.Fatalf("Expected %v but got %v", testCase.expected, b) + } + }) } } diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..c74a1d6 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,10 @@ +module github.com/xanzy/go-gitlab + +require ( + github.com/google/go-querystring v1.0.0 + github.com/stretchr/testify v1.3.0 + golang.org/x/net v0.0.0-20181108082009-03003ca0c849 // indirect + golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 + golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect + google.golang.org/appengine v1.3.0 // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..ef4a85e --- /dev/null +++ b/api/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181108082009-03003ca0c849 h1:FSqE2GGG7wzsYUsWiQ8MZrvEd1EOyU3NCF0AW3Wtltg= +golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/api/group_badges.go b/api/group_badges.go new file mode 100644 index 0000000..61981a6 --- /dev/null +++ b/api/group_badges.go @@ -0,0 +1,214 @@ +package gitlab + +import ( + "fmt" + "net/url" +) + +// GroupBadgesService handles communication with the group badges +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html +type GroupBadgesService struct { + client *Client +} + +// BadgeKind represents a GitLab Badge Kind +type BadgeKind string + +// all possible values Badge Kind +const ( + ProjectBadgeKind BadgeKind = "project" + GroupBadgeKind BadgeKind = "group" +) + +// GroupBadge represents a group badge. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html +type GroupBadge struct { + ID int `json:"id"` + LinkURL string `json:"link_url"` + ImageURL string `json:"image_url"` + RenderedLinkURL string `json:"rendered_link_url"` + RenderedImageURL string `json:"rendered_image_url"` + Kind BadgeKind `json:"kind"` +} + +// ListGroupBadgesOptions represents the available ListGroupBadges() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html#list-all-badges-of-a-group +type ListGroupBadgesOptions ListOptions + +// ListGroupBadges gets a list of a group badges. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html#list-all-badges-of-a-group +func (s *GroupBadgesService) ListGroupBadges(gid interface{}, opt *ListGroupBadgesOptions, options ...OptionFunc) ([]*GroupBadge, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/badges", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var gb []*GroupBadge + resp, err := s.client.Do(req, &gb) + if err != nil { + return nil, resp, err + } + + return gb, resp, err +} + +// GetGroupBadge gets a group badge. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html#get-a-badge-of-a-group +func (s *GroupBadgesService) GetGroupBadge(gid interface{}, badge int, options ...OptionFunc) (*GroupBadge, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/badges/%d", url.QueryEscape(group), badge) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + gb := new(GroupBadge) + resp, err := s.client.Do(req, gb) + if err != nil { + return nil, resp, err + } + + return gb, resp, err +} + +// AddGroupBadgeOptions represents the available AddGroupBadge() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html#add-a-badge-to-a-group +type AddGroupBadgeOptions struct { + LinkURL *string `url:"link_url,omitempty" json:"link_url,omitempty"` + ImageURL *string `url:"image_url,omitempty" json:"image_url,omitempty"` +} + +// AddGroupBadge adds a badge to a group. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html#add-a-badge-to-a-group +func (s *GroupBadgesService) AddGroupBadge(gid interface{}, opt *AddGroupBadgeOptions, options ...OptionFunc) (*GroupBadge, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/badges", url.QueryEscape(group)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + gb := new(GroupBadge) + resp, err := s.client.Do(req, gb) + if err != nil { + return nil, resp, err + } + + return gb, resp, err +} + +// EditGroupBadgeOptions represents the available EditGroupBadge() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html#edit-a-badge-of-a-group +type EditGroupBadgeOptions struct { + LinkURL *string `url:"link_url,omitempty" json:"link_url,omitempty"` + ImageURL *string `url:"image_url,omitempty" json:"image_url,omitempty"` +} + +// EditGroupBadge updates a badge of a group. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html#edit-a-badge-of-a-group +func (s *GroupBadgesService) EditGroupBadge(gid interface{}, badge int, opt *EditGroupBadgeOptions, options ...OptionFunc) (*GroupBadge, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/badges/%d", url.QueryEscape(group), badge) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + gb := new(GroupBadge) + resp, err := s.client.Do(req, gb) + if err != nil { + return nil, resp, err + } + + return gb, resp, err +} + +// DeleteGroupBadge removes a badge from a group. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html#remove-a-badge-from-a-group +func (s *GroupBadgesService) DeleteGroupBadge(gid interface{}, badge int, options ...OptionFunc) (*Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("groups/%s/badges/%d", url.QueryEscape(group), badge) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// GroupBadgePreviewOptions represents the available PreviewGroupBadge() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html#preview-a-badge-from-a-group +type GroupBadgePreviewOptions struct { + LinkURL *string `url:"link_url,omitempty" json:"link_url,omitempty"` + ImageURL *string `url:"image_url,omitempty" json:"image_url,omitempty"` +} + +// PreviewGroupBadge returns how the link_url and image_url final URLs would be after +// resolving the placeholder interpolation. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_badges.html#preview-a-badge-from-a-group +func (s *GroupBadgesService) PreviewGroupBadge(gid interface{}, opt *GroupBadgePreviewOptions, options ...OptionFunc) (*GroupBadge, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/badges/render", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + gb := new(GroupBadge) + resp, err := s.client.Do(req, &gb) + if err != nil { + return nil, resp, err + } + + return gb, resp, err +} diff --git a/api/group_boards.go b/api/group_boards.go new file mode 100644 index 0000000..764df14 --- /dev/null +++ b/api/group_boards.go @@ -0,0 +1,262 @@ +// +// Copyright 2018, Patrick Webster +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// GroupIssueBoardsService handles communication with the group issue board +// related methods of the GitLab API. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html +type GroupIssueBoardsService struct { + client *Client +} + +// GroupIssueBoard represents a GitLab group issue board. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html +type GroupIssueBoard struct { + ID int `json:"id"` + Name string `json:"name"` + Group *Group `json:"group"` + Milestone *Milestone `json:"milestone"` + Lists []*BoardList `json:"lists"` +} + +func (b GroupIssueBoard) String() string { + return Stringify(b) +} + +// ListGroupIssueBoardsOptions represents the available +// ListGroupIssueBoards() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html#group-board +type ListGroupIssueBoardsOptions ListOptions + +// ListGroupIssueBoards gets a list of all issue boards in a group. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html#group-board +func (s *GroupIssueBoardsService) ListGroupIssueBoards(gid interface{}, opt *ListGroupIssueBoardsOptions, options ...OptionFunc) ([]*GroupIssueBoard, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/boards", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var gs []*GroupIssueBoard + resp, err := s.client.Do(req, &gs) + if err != nil { + return nil, resp, err + } + + return gs, resp, err +} + +// GetGroupIssueBoard gets a single issue board of a group. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html#single-board +func (s *GroupIssueBoardsService) GetGroupIssueBoard(gid interface{}, board int, options ...OptionFunc) (*GroupIssueBoard, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/boards/%d", url.QueryEscape(group), board) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + gib := new(GroupIssueBoard) + resp, err := s.client.Do(req, gib) + if err != nil { + return nil, resp, err + } + + return gib, resp, err +} + +// ListGroupIssueBoardListsOptions represents the available +// ListGroupIssueBoardLists() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html#list-board-lists +type ListGroupIssueBoardListsOptions ListOptions + +// ListGroupIssueBoardLists gets a list of the issue board's lists. Does not include +// backlog and closed lists. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/group_boards.html#list-board-lists +func (s *GroupIssueBoardsService) ListGroupIssueBoardLists(gid interface{}, board int, opt *ListGroupIssueBoardListsOptions, options ...OptionFunc) ([]*BoardList, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/boards/%d/lists", url.QueryEscape(group), board) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var gbl []*BoardList + resp, err := s.client.Do(req, &gbl) + if err != nil { + return nil, resp, err + } + + return gbl, resp, err +} + +// GetGroupIssueBoardList gets a single issue board list. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html#single-board-list +func (s *GroupIssueBoardsService) GetGroupIssueBoardList(gid interface{}, board, list int, options ...OptionFunc) (*BoardList, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/boards/%d/lists/%d", + url.QueryEscape(group), + board, + list, + ) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + gbl := new(BoardList) + resp, err := s.client.Do(req, gbl) + if err != nil { + return nil, resp, err + } + + return gbl, resp, err +} + +// CreateGroupIssueBoardListOptions represents the available +// CreateGroupIssueBoardList() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html#new-board-list +type CreateGroupIssueBoardListOptions struct { + LabelID *int `url:"label_id" json:"label_id"` +} + +// CreateGroupIssueBoardList creates a new issue board list. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html#new-board-list +func (s *GroupIssueBoardsService) CreateGroupIssueBoardList(gid interface{}, board int, opt *CreateGroupIssueBoardListOptions, options ...OptionFunc) (*BoardList, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/boards/%d/lists", url.QueryEscape(group), board) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + gbl := new(BoardList) + resp, err := s.client.Do(req, gbl) + if err != nil { + return nil, resp, err + } + + return gbl, resp, err +} + +// UpdateGroupIssueBoardListOptions represents the available +// UpdateGroupIssueBoardList() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html#edit-board-list +type UpdateGroupIssueBoardListOptions struct { + Position *int `url:"position" json:"position"` +} + +// UpdateIssueBoardList updates the position of an existing +// group issue board list. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html#edit-board-list +func (s *GroupIssueBoardsService) UpdateIssueBoardList(gid interface{}, board, list int, opt *UpdateGroupIssueBoardListOptions, options ...OptionFunc) ([]*BoardList, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/boards/%d/lists/%d", + url.QueryEscape(group), + board, + list, + ) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + var gbl []*BoardList + resp, err := s.client.Do(req, gbl) + if err != nil { + return nil, resp, err + } + + return gbl, resp, err +} + +// DeleteGroupIssueBoardList soft deletes a group issue board list. +// Only for admins and group owners. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_boards.html#delete-a-board-list +func (s *GroupIssueBoardsService) DeleteGroupIssueBoardList(gid interface{}, board, list int, options ...OptionFunc) (*Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("groups/%s/boards/%d/lists/%d", + url.QueryEscape(group), + board, + list, + ) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/group_members.go b/api/group_members.go new file mode 100644 index 0000000..62d968a --- /dev/null +++ b/api/group_members.go @@ -0,0 +1,220 @@ +// +// Copyright 2017, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// GroupMembersService handles communication with the group members +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/members.html +type GroupMembersService struct { + client *Client +} + +// GroupMember represents a GitLab group member. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/members.html +type GroupMember struct { + ID int `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + ExpiresAt *ISOTime `json:"expires_at"` + AccessLevel AccessLevelValue `json:"access_level"` +} + +// ListGroupMembersOptions represents the available ListGroupMembers() and +// ListAllGroupMembers() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project +type ListGroupMembersOptions struct { + ListOptions + Query *string `url:"query,omitempty" json:"query,omitempty"` +} + +// ListGroupMembers get a list of group members viewable by the authenticated +// user. Returns a list including inherited members through ancestor groups. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project +func (s *GroupsService) ListGroupMembers(gid interface{}, opt *ListGroupMembersOptions, options ...OptionFunc) ([]*GroupMember, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/members", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var gm []*GroupMember + resp, err := s.client.Do(req, &gm) + if err != nil { + return nil, resp, err + } + + return gm, resp, err +} + +// ListAllGroupMembers get a list of group members viewable by the authenticated +// user. Returns a list including inherited members through ancestor groups. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project-including-inherited-members +func (s *GroupsService) ListAllGroupMembers(gid interface{}, opt *ListGroupMembersOptions, options ...OptionFunc) ([]*GroupMember, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/members/all", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var gm []*GroupMember + resp, err := s.client.Do(req, &gm) + if err != nil { + return nil, resp, err + } + + return gm, resp, err +} + +// AddGroupMemberOptions represents the available AddGroupMember() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#add-a-member-to-a-group-or-project +type AddGroupMemberOptions struct { + UserID *int `url:"user_id,omitempty" json:"user_id,omitempty"` + AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"` + ExpiresAt *string `url:"expires_at,omitempty" json:"expires_at"` +} + +// GetGroupMember gets a member of a group. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#get-a-member-of-a-group-or-project +func (s *GroupMembersService) GetGroupMember(gid interface{}, user int, options ...OptionFunc) (*GroupMember, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/members/%d", url.QueryEscape(group), user) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + gm := new(GroupMember) + resp, err := s.client.Do(req, gm) + if err != nil { + return nil, resp, err + } + + return gm, resp, err +} + +// AddGroupMember adds a user to the list of group members. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#add-a-member-to-a-group-or-project +func (s *GroupMembersService) AddGroupMember(gid interface{}, opt *AddGroupMemberOptions, options ...OptionFunc) (*GroupMember, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/members", url.QueryEscape(group)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + gm := new(GroupMember) + resp, err := s.client.Do(req, gm) + if err != nil { + return nil, resp, err + } + + return gm, resp, err +} + +// EditGroupMemberOptions represents the available EditGroupMember() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#edit-a-member-of-a-group-or-project +type EditGroupMemberOptions struct { + AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"` + ExpiresAt *string `url:"expires_at,omitempty" json:"expires_at"` +} + +// EditGroupMember updates a member of a group. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#edit-a-member-of-a-group-or-project +func (s *GroupMembersService) EditGroupMember(gid interface{}, user int, opt *EditGroupMemberOptions, options ...OptionFunc) (*GroupMember, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/members/%d", url.QueryEscape(group), user) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + gm := new(GroupMember) + resp, err := s.client.Do(req, gm) + if err != nil { + return nil, resp, err + } + + return gm, resp, err +} + +// RemoveGroupMember removes user from user team. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#remove-a-member-from-a-group-or-project +func (s *GroupMembersService) RemoveGroupMember(gid interface{}, user int, options ...OptionFunc) (*Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("groups/%s/members/%d", url.QueryEscape(group), user) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/group_milestones.go b/api/group_milestones.go new file mode 100644 index 0000000..8335a47 --- /dev/null +++ b/api/group_milestones.go @@ -0,0 +1,250 @@ +// +// Copyright 2018, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// GroupMilestonesService handles communication with the milestone related +// methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/group_milestones.html +type GroupMilestonesService struct { + client *Client +} + +// GroupMilestone represents a GitLab milestone. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/group_milestones.html +type GroupMilestone struct { + ID int `json:"id"` + IID int `json:"iid"` + GroupID int `json:"group_id"` + Title string `json:"title"` + Description string `json:"description"` + StartDate *ISOTime `json:"start_date"` + DueDate *ISOTime `json:"due_date"` + State string `json:"state"` + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` +} + +func (m GroupMilestone) String() string { + return Stringify(m) +} + +// ListGroupMilestonesOptions represents the available +// ListGroupMilestones() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#list-group-milestones +type ListGroupMilestonesOptions struct { + ListOptions + IIDs []int `url:"iids,omitempty" json:"iids,omitempty"` + State string `url:"state,omitempty" json:"state,omitempty"` + Search string `url:"search,omitempty" json:"search,omitempty"` +} + +// ListGroupMilestones returns a list of group milestones. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#list-group-milestones +func (s *GroupMilestonesService) ListGroupMilestones(gid interface{}, opt *ListGroupMilestonesOptions, options ...OptionFunc) ([]*GroupMilestone, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/milestones", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var m []*GroupMilestone + resp, err := s.client.Do(req, &m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// GetGroupMilestone gets a single group milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#get-single-milestone +func (s *GroupMilestonesService) GetGroupMilestone(gid interface{}, milestone int, options ...OptionFunc) (*GroupMilestone, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/milestones/%d", url.QueryEscape(group), milestone) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + m := new(GroupMilestone) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// CreateGroupMilestoneOptions represents the available CreateGroupMilestone() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#create-new-milestone +type CreateGroupMilestoneOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + StartDate *ISOTime `url:"start_date,omitempty" json:"start_date,omitempty"` + DueDate *ISOTime `url:"due_date,omitempty" json:"due_date,omitempty"` +} + +// CreateGroupMilestone creates a new group milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#create-new-milestone +func (s *GroupMilestonesService) CreateGroupMilestone(gid interface{}, opt *CreateGroupMilestoneOptions, options ...OptionFunc) (*GroupMilestone, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/milestones", url.QueryEscape(group)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + m := new(GroupMilestone) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// UpdateGroupMilestoneOptions represents the available UpdateGroupMilestone() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#edit-milestone +type UpdateGroupMilestoneOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + StartDate *ISOTime `url:"start_date,omitempty" json:"start_date,omitempty"` + DueDate *ISOTime `url:"due_date,omitempty" json:"due_date,omitempty"` + StateEvent *string `url:"state_event,omitempty" json:"state_event,omitempty"` +} + +// UpdateGroupMilestone updates an existing group milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#edit-milestone +func (s *GroupMilestonesService) UpdateGroupMilestone(gid interface{}, milestone int, opt *UpdateGroupMilestoneOptions, options ...OptionFunc) (*GroupMilestone, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/milestones/%d", url.QueryEscape(group), milestone) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + m := new(GroupMilestone) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// GetGroupMilestoneIssuesOptions represents the available GetGroupMilestoneIssues() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#get-all-issues-assigned-to-a-single-milestone +type GetGroupMilestoneIssuesOptions ListOptions + +// GetGroupMilestoneIssues gets all issues assigned to a single group milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#get-all-issues-assigned-to-a-single-milestone +func (s *GroupMilestonesService) GetGroupMilestoneIssues(gid interface{}, milestone int, opt *GetGroupMilestoneIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/milestones/%d/issues", url.QueryEscape(group), milestone) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var i []*Issue + resp, err := s.client.Do(req, &i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + +// GetGroupMilestoneMergeRequestsOptions represents the available +// GetGroupMilestoneMergeRequests() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#get-all-merge-requests-assigned-to-a-single-milestone +type GetGroupMilestoneMergeRequestsOptions ListOptions + +// GetGroupMilestoneMergeRequests gets all merge requests assigned to a +// single group milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/group_milestones.html#get-all-merge-requests-assigned-to-a-single-milestone +func (s *GroupMilestonesService) GetGroupMilestoneMergeRequests(gid interface{}, milestone int, opt *GetGroupMilestoneMergeRequestsOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/milestones/%d/merge_requests", url.QueryEscape(group), milestone) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var mr []*MergeRequest + resp, err := s.client.Do(req, &mr) + if err != nil { + return nil, resp, err + } + + return mr, resp, err +} diff --git a/api/group_variables.go b/api/group_variables.go new file mode 100644 index 0000000..cc28e4f --- /dev/null +++ b/api/group_variables.go @@ -0,0 +1,171 @@ +// +// Copyright 2018, Patrick Webster +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// GroupVariablesService handles communication with the +// group variables related methods of the GitLab API. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_level_variables.html +type GroupVariablesService struct { + client *Client +} + +// GroupVariable represents a GitLab group Variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_level_variables.html +type GroupVariable struct { + Key string `json:"key"` + Value string `json:"value"` + Protected bool `json:"protected"` +} + +func (v GroupVariable) String() string { + return Stringify(v) +} + +// ListVariables gets a list of all variables for a group. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_level_variables.html#list-group-variables +func (s *GroupVariablesService) ListVariables(gid interface{}, options ...OptionFunc) ([]*GroupVariable, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/variables", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var vs []*GroupVariable + resp, err := s.client.Do(req, &vs) + if err != nil { + return nil, resp, err + } + + return vs, resp, err +} + +// GetVariable gets a variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_level_variables.html#show-variable-details +func (s *GroupVariablesService) GetVariable(gid interface{}, key string, options ...OptionFunc) (*GroupVariable, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/variables/%s", url.QueryEscape(group), url.QueryEscape(key)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + v := new(GroupVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// CreateVariable creates a new group variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_level_variables.html#create-variable +func (s *GroupVariablesService) CreateVariable(gid interface{}, opt *CreateVariableOptions, options ...OptionFunc) (*GroupVariable, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/variables", url.QueryEscape(group)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + v := new(GroupVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// UpdateVariable updates the position of an existing +// group issue board list. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_level_variables.html#update-variable +func (s *GroupVariablesService) UpdateVariable(gid interface{}, key string, opt *UpdateVariableOptions, options ...OptionFunc) (*GroupVariable, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/variables/%s", + url.QueryEscape(group), + url.QueryEscape(key), + ) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + v := new(GroupVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// RemoveVariable removes a group's variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_level_variables.html#remove-variable +func (s *GroupVariablesService) RemoveVariable(gid interface{}, key string, options ...OptionFunc) (*Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("groups/%s/variables/%s", + url.QueryEscape(group), + url.QueryEscape(key), + ) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/groups.go b/api/groups.go index de39dc2..200f07d 100644 --- a/api/groups.go +++ b/api/groups.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,42 +18,60 @@ package gitlab import ( "fmt" - "time" + "net/url" ) // GroupsService handles communication with the group related methods of // the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/groups.html +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html type GroupsService struct { client *Client } // Group represents a GitLab group. // -// GitLab API docs: http://doc.gitlab.com/ce/api/groups.html +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html type Group struct { - ID int `json:"id"` - Name string `json:"name"` - Path string `json:"path"` - Description string `json:"description"` - Projects *[]Project `json:"projects,omitempty"` + ID int `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Description string `json:"description"` + Visibility *VisibilityValue `json:"visibility"` + LFSEnabled bool `json:"lfs_enabled"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + RequestAccessEnabled bool `json:"request_access_enabled"` + FullName string `json:"full_name"` + FullPath string `json:"full_path"` + ParentID int `json:"parent_id"` + Projects []*Project `json:"projects"` + Statistics *StorageStatistics `json:"statistics"` + CustomAttributes []*CustomAttribute `json:"custom_attributes"` } // ListGroupsOptions represents the available ListGroups() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/groups.html#list-project-groups +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#list-project-groups type ListGroupsOptions struct { ListOptions - Search string `url:"search,omitempty" json:"search,omitempty"` + AllAvailable *bool `url:"all_available,omitempty" json:"all_available,omitempty"` + MinAccessLevel *AccessLevelValue `url:"min_access_level,omitempty" json:"min_access_level,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Owned *bool `url:"owned,omitempty" json:"owned,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` + SkipGroups []int `url:"skip_groups,omitempty" json:"skip_groups,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` + Statistics *bool `url:"statistics,omitempty" json:"statistics,omitempty"` + WithCustomAttributes *bool `url:"with_custom_attributes,omitempty" json:"with_custom_attributes,omitempty"` } -// ListGroups gets a list of groups. (As user: my groups, as admin: all groups) +// ListGroups gets a list of groups (as user: my groups, as admin: all groups). // // GitLab API docs: -// http://doc.gitlab.com/ce/api/groups.html#list-project-groups -func (s *GroupsService) ListGroups(opt *ListGroupsOptions) ([]*Group, *Response, error) { - req, err := s.client.NewRequest("GET", "groups", opt) +// https://docs.gitlab.com/ce/api/groups.html#list-project-groups +func (s *GroupsService) ListGroups(opt *ListGroupsOptions, options ...OptionFunc) ([]*Group, *Response, error) { + req, err := s.client.NewRequest("GET", "groups", opt, options) if err != nil { return nil, nil, err } @@ -69,15 +87,15 @@ func (s *GroupsService) ListGroups(opt *ListGroupsOptions) ([]*Group, *Response, // GetGroup gets all details of a group. // -// GitLab API docs: http://doc.gitlab.com/ce/api/groups.html#details-of-a-group -func (s *GroupsService) GetGroup(gid interface{}) (*Group, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#details-of-a-group +func (s *GroupsService) GetGroup(gid interface{}, options ...OptionFunc) (*Group, *Response, error) { group, err := parseID(gid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("groups/%s", group) + u := fmt.Sprintf("groups/%s", url.QueryEscape(group)) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -93,19 +111,23 @@ func (s *GroupsService) GetGroup(gid interface{}) (*Group, *Response, error) { // CreateGroupOptions represents the available CreateGroup() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/groups.html#new-group +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#new-group type CreateGroupOptions struct { - Name string `url:"name,omitempty" json:"name,omitempty"` - Path string `url:"path,omitempty" json:"path,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + Path *string `url:"path,omitempty" json:"path,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + Visibility *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"` + LFSEnabled *bool `url:"lfs_enabled,omitempty" json:"lfs_enabled,omitempty"` + RequestAccessEnabled *bool `url:"request_access_enabled,omitempty" json:"request_access_enabled,omitempty"` + ParentID *int `url:"parent_id,omitempty" json:"parent_id,omitempty"` } // CreateGroup creates a new project group. Available only for users who can // create groups. // -// GitLab API docs: http://doc.gitlab.com/ce/api/groups.html#new-group -func (s *GroupsService) CreateGroup(opt *CreateGroupOptions) (*Group, *Response, error) { - req, err := s.client.NewRequest("POST", "groups", opt) +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#new-group +func (s *GroupsService) CreateGroup(opt *CreateGroupOptions, options ...OptionFunc) (*Group, *Response, error) { + req, err := s.client.NewRequest("POST", "groups", opt, options) if err != nil { return nil, nil, err } @@ -123,15 +145,22 @@ func (s *GroupsService) CreateGroup(opt *CreateGroupOptions) (*Group, *Response, // for admin. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/groups.html#transfer-project-to-group -func (s *GroupsService) TransferGroup(gid interface{}, project int) (*Group, *Response, error) { +// https://docs.gitlab.com/ce/api/groups.html#transfer-project-to-group +func (s *GroupsService) TransferGroup(gid interface{}, pid interface{}, options ...OptionFunc) (*Group, *Response, error) { group, err := parseID(gid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("groups/%s/projects/%d", group, project) - req, err := s.client.NewRequest("POST", u, nil) + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + + u := fmt.Sprintf("groups/%s/projects/%s", url.QueryEscape(group), + url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { return nil, nil, err } @@ -145,83 +174,70 @@ func (s *GroupsService) TransferGroup(gid interface{}, project int) (*Group, *Re return g, resp, err } -// DeleteGroup removes group with all projects inside. +// UpdateGroupOptions represents the set of available options to update a Group; +// as of today these are exactly the same available when creating a new Group. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#update-group +type UpdateGroupOptions CreateGroupOptions + +// UpdateGroup updates an existing group; only available to group owners and +// administrators. // -// GitLab API docs: http://doc.gitlab.com/ce/api/groups.html#remove-group -func (s *GroupsService) DeleteGroup(gid interface{}) (*Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#update-group +func (s *GroupsService) UpdateGroup(gid interface{}, opt *UpdateGroupOptions, options ...OptionFunc) (*Group, *Response, error) { group, err := parseID(gid) if err != nil { - return nil, err + return nil, nil, err } - u := fmt.Sprintf("groups/%s", group) + u := fmt.Sprintf("groups/%s", url.QueryEscape(group)) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { - return nil, err + return nil, nil, err } - resp, err := s.client.Do(req, nil) + g := new(Group) + resp, err := s.client.Do(req, g) if err != nil { - return resp, err + return nil, resp, err } - return resp, err + return g, resp, err } -// SearchGroup get all groups that match your string in their name or path. +// DeleteGroup removes group with all projects inside. // -// GitLab API docs: http://doc.gitlab.com/ce/api/groups.html#search-for-group -func (s *GroupsService) SearchGroup(query string) ([]*Group, *Response, error) { - var q struct { - Search string `url:"search,omitempty" json:"search,omitempty"` - } - q.Search = query - - req, err := s.client.NewRequest("GET", "groups", &q) +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#remove-group +func (s *GroupsService) DeleteGroup(gid interface{}, options ...OptionFunc) (*Response, error) { + group, err := parseID(gid) if err != nil { - return nil, nil, err + return nil, err } + u := fmt.Sprintf("groups/%s", url.QueryEscape(group)) - var g []*Group - resp, err := s.client.Do(req, &g) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { - return nil, resp, err + return nil, err } - return g, resp, err -} - -// GroupMember represents a GitLab group member. -// -// GitLab API docs: http://doc.gitlab.com/ce/api/groups.html -type GroupMember struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - State string `json:"state"` - CreatedAt time.Time `json:"created_at"` - AccessLevel int `json:"access_level"` + return s.client.Do(req, nil) } -// ListGroupMembers get a list of group members viewable by the authenticated -// user. +// SearchGroup get all groups that match your string in their name or path. // -// GitLab API docs: -// http://doc.gitlab.com/ce/api/groups.html#list-group-members -func (s *GroupsService) ListGroupMembers(gid interface{}) ([]*GroupMember, *Response, error) { - group, err := parseID(gid) - if err != nil { - return nil, nil, err +// GitLab API docs: https://docs.gitlab.com/ce/api/groups.html#search-for-group +func (s *GroupsService) SearchGroup(query string, options ...OptionFunc) ([]*Group, *Response, error) { + var q struct { + Search string `url:"search,omitempty" json:"search,omitempty"` } - u := fmt.Sprintf("groups/%s/members", group) + q.Search = query - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", "groups", &q, options) if err != nil { return nil, nil, err } - var g []*GroupMember + var g []*Group resp, err := s.client.Do(req, &g) if err != nil { return nil, resp, err @@ -230,98 +246,66 @@ func (s *GroupsService) ListGroupMembers(gid interface{}) ([]*GroupMember, *Resp return g, resp, err } -// AddGroupMemberOptions represents the available AddGroupMember() options. +// ListGroupProjectsOptions represents the available ListGroupProjects() +// options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/groups.html#add-group-member -type AddGroupMemberOptions struct { - UserID int `url:"user_id,omitempty" json:"user_id,omitempty"` - AccessLevel AccessLevel `url:"access_level,omitempty" json:"access_level,omitempty"` -} +// GitLab API docs: +// https://docs.gitlab.com/ce/api/groups.html#list-a-group-39-s-projects +type ListGroupProjectsOptions ListProjectsOptions -// AddGroupMember adds a user to the list of group members. +// ListGroupProjects get a list of group projects // // GitLab API docs: -// http://doc.gitlab.com/ce/api/groups.html#list-group-members -func (s *GroupsService) AddGroupMember( - gid interface{}, - opt *AddGroupMemberOptions) (*GroupMember, *Response, error) { +// https://docs.gitlab.com/ce/api/groups.html#list-a-group-39-s-projects +func (s *GroupsService) ListGroupProjects(gid interface{}, opt *ListGroupProjectsOptions, options ...OptionFunc) ([]*Project, *Response, error) { group, err := parseID(gid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("groups/%s/members", group) + u := fmt.Sprintf("groups/%s/projects", url.QueryEscape(group)) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } - g := new(GroupMember) - resp, err := s.client.Do(req, g) + var p []*Project + resp, err := s.client.Do(req, &p) if err != nil { return nil, resp, err } - return g, resp, err + return p, resp, err } -// UpdateGroupMemberOptions represents the available UpdateGroupMember() +// ListSubgroupsOptions represents the available ListSubgroupsOptions() // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/groups.html#edit-group-team-member -type UpdateGroupMemberOptions struct { - AccessLevel AccessLevel `url:"access_level,omitempty" json:"access_level,omitempty"` -} +// https://docs.gitlab.com/ce/api/groups.html#list-a-groups-s-subgroups +type ListSubgroupsOptions ListGroupsOptions -// UpdateGroupMember updates a group team member to a specified access level. +// ListSubgroups gets a list of subgroups for a given project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/groups.html#list-group-members -func (s *GroupsService) UpdateGroupMember( - gid interface{}, - user int, - opt *UpdateGroupMemberOptions) (*GroupMember, *Response, error) { +// https://docs.gitlab.com/ce/api/groups.html#list-a-groups-s-subgroups +func (s *GroupsService) ListSubgroups(gid interface{}, opt *ListSubgroupsOptions, options ...OptionFunc) ([]*Group, *Response, error) { group, err := parseID(gid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("groups/%s/members/%d", group, user) + u := fmt.Sprintf("groups/%s/subgroups", url.QueryEscape(group)) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } - g := new(GroupMember) - resp, err := s.client.Do(req, g) + var g []*Group + resp, err := s.client.Do(req, &g) if err != nil { return nil, resp, err } return g, resp, err } - -// RemoveGroupMember removes user from user team. -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/groups.html#remove-user-from-user-team -func (s *GroupsService) RemoveGroupMember(gid interface{}, user int) (*Response, error) { - group, err := parseID(gid) - if err != nil { - return nil, err - } - u := fmt.Sprintf("groups/%s/members/%d", group, user) - - req, err := s.client.NewRequest("DELETE", u, nil) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - return resp, err - } - - return resp, err -} diff --git a/api/groups_badges_test.go b/api/groups_badges_test.go new file mode 100644 index 0000000..40f6c19 --- /dev/null +++ b/api/groups_badges_test.go @@ -0,0 +1,118 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestListGroupBadges(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/1/badges", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1, "kind":"group"},{"id":2, "kind":"group"}]`) + }) + + badges, _, err := client.GroupBadges.ListGroupBadges(1, &ListGroupBadgesOptions{}) + if err != nil { + t.Errorf("GroupBadges.ListGroupBadges returned error: %v", err) + } + + want := []*GroupBadge{{ID: 1, Kind: GroupBadgeKind}, {ID: 2, Kind: GroupBadgeKind}} + if !reflect.DeepEqual(want, badges) { + t.Errorf("GroupBadges.ListGroupBadges returned %+v, want %+v", badges, want) + } +} + +func TestGetGroupBadge(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/1/badges/2", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":2, "kind":"group"}`) + }) + + badge, _, err := client.GroupBadges.GetGroupBadge(1, 2) + if err != nil { + t.Errorf("GroupBadges.GetGroupBadge returned error: %v", err) + } + + want := &GroupBadge{ID: 2, Kind: GroupBadgeKind} + if !reflect.DeepEqual(want, badge) { + t.Errorf("GroupBadges.GetGroupBadge returned %+v, want %+v", badge, want) + } +} + +func TestAddGroupBadge(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/1/badges", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id":3, "link_url":"LINK", "image_url":"IMAGE", "kind":"group"}`) + }) + + opt := &AddGroupBadgeOptions{ImageURL: String("IMAGE"), LinkURL: String("LINK")} + badge, _, err := client.GroupBadges.AddGroupBadge(1, opt) + if err != nil { + t.Errorf("GroupBadges.AddGroupBadge returned error: %v", err) + } + + want := &GroupBadge{ID: 3, ImageURL: "IMAGE", LinkURL: "LINK", Kind: GroupBadgeKind} + if !reflect.DeepEqual(want, badge) { + t.Errorf("GroupBadges.AddGroupBadge returned %+v, want %+v", badge, want) + } +} + +func TestEditGroupBadge(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/1/badges/2", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{"id":2, "link_url":"NEW_LINK", "image_url":"NEW_IMAGE", "kind":"group"}`) + }) + + opt := &EditGroupBadgeOptions{ImageURL: String("NEW_IMAGE"), LinkURL: String("NEW_LINK")} + badge, _, err := client.GroupBadges.EditGroupBadge(1, 2, opt) + if err != nil { + t.Errorf("GroupBadges.EditGroupBadge returned error: %v", err) + } + + want := &GroupBadge{ID: 2, ImageURL: "NEW_IMAGE", LinkURL: "NEW_LINK", Kind: GroupBadgeKind} + if !reflect.DeepEqual(want, badge) { + t.Errorf("GroupBadges.EditGroupBadge returned %+v, want %+v", badge, want) + } +} + +func TestRemoveGroupBadge(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/1/badges/2", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusAccepted) + }, + ) + + resp, err := client.GroupBadges.DeleteGroupBadge(1, 2) + if err != nil { + t.Errorf("GroupBadges.DeleteGroupBadge returned error: %v", err) + } + + want := http.StatusAccepted + got := resp.StatusCode + if got != want { + t.Errorf("GroupsBadges.DeleteGroupBadge returned %d, want %d", got, want) + } + +} diff --git a/api/groups_test.go b/api/groups_test.go new file mode 100644 index 0000000..99c00b9 --- /dev/null +++ b/api/groups_test.go @@ -0,0 +1,206 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestListGroups(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1},{"id":2}]`) + }) + + groups, _, err := client.Groups.ListGroups(&ListGroupsOptions{}) + if err != nil { + t.Errorf("Groups.ListGroups returned error: %v", err) + } + + want := []*Group{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(want, groups) { + t.Errorf("Groups.ListGroups returned %+v, want %+v", groups, want) + } +} + +func TestGetGroup(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/g", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id": 1, "name": "g"}`) + }) + + group, _, err := client.Groups.GetGroup("g") + if err != nil { + t.Errorf("Groups.GetGroup returned error: %v", err) + } + + want := &Group{ID: 1, Name: "g"} + if !reflect.DeepEqual(want, group) { + t.Errorf("Groups.GetGroup returned %+v, want %+v", group, want) + } +} + +func TestCreateGroup(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id": 1, "name": "g", "path": "g"}`) + }) + + opt := &CreateGroupOptions{ + Name: String("g"), + Path: String("g"), + } + + group, _, err := client.Groups.CreateGroup(opt, nil) + if err != nil { + t.Errorf("Groups.CreateGroup returned error: %v", err) + } + + want := &Group{ID: 1, Name: "g", Path: "g"} + if !reflect.DeepEqual(want, group) { + t.Errorf("Groups.CreateGroup returned %+v, want %+v", group, want) + } +} + +func TestTransferGroup(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/1/projects/2", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprintf(w, `{"id": 1}`) + }) + + group, _, err := client.Groups.TransferGroup(1, 2) + if err != nil { + t.Errorf("Groups.TransferGroup returned error: %v", err) + } + + want := &Group{ID: 1} + if !reflect.DeepEqual(group, want) { + t.Errorf("Groups.TransferGroup returned %+v, want %+v", group, want) + } + +} + +func TestDeleteGroup(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/1", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusAccepted) + }) + + resp, err := client.Groups.DeleteGroup(1) + if err != nil { + t.Errorf("Groups.DeleteGroup returned error: %v", err) + } + fmt.Println(resp.StatusCode) + + want := http.StatusAccepted + got := resp.StatusCode + if got != want { + t.Errorf("Groups.DeleteGroup returned %d, want %d", got, want) + } +} + +func TestSearchGroup(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id": 1, "name": "Foobar Group"}]`) + }) + + groups, _, err := client.Groups.SearchGroup("foobar") + if err != nil { + t.Errorf("Groups.SearchGroup returned error: %v", err) + } + + want := []*Group{{ID: 1, Name: "Foobar Group"}} + if !reflect.DeepEqual(want, groups) { + t.Errorf("Groups.SearchGroup returned +%v, want %+v", groups, want) + } +} + +func TestUpdateGroup(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/1", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{"id": 1}`) + }) + + group, _, err := client.Groups.UpdateGroup(1, &UpdateGroupOptions{}) + if err != nil { + t.Errorf("Groups.UpdateGroup returned error: %v", err) + } + + want := &Group{ID: 1} + if !reflect.DeepEqual(want, group) { + t.Errorf("Groups.UpdatedGroup returned %+v, want %+v", group, want) + } +} + +func TestListGroupProjects(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/22/projects", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1},{"id":2}]`) + }) + + projects, _, err := client.Groups.ListGroupProjects(22, + &ListGroupProjectsOptions{}) + if err != nil { + t.Errorf("Groups.ListGroupProjects returned error: %v", err) + } + + want := []*Project{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(want, projects) { + t.Errorf("Groups.ListGroupProjects returned %+v, want %+v", projects, want) + } +} + +func TestListSubgroups(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/1/subgroups", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id": 1}, {"id": 2}]`) + }) + + groups, _, err := client.Groups.ListSubgroups(1, &ListSubgroupsOptions{}) + if err != nil { + t.Errorf("Groups.ListSubgroups returned error: %v", err) + } + + want := []*Group{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(want, groups) { + t.Errorf("Groups.ListSubgroups returned %+v, want %+v", groups, want) + } +} diff --git a/api/issue_links.go b/api/issue_links.go new file mode 100644 index 0000000..5dfd76a --- /dev/null +++ b/api/issue_links.go @@ -0,0 +1,128 @@ +// +// Copyright 2017, Arkbriar +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// IssueLinksService handles communication with the issue relations related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/issue_links.html +type IssueLinksService struct { + client *Client +} + +// IssueLink represents a two-way relation between two issues. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/issue_links.html +type IssueLink struct { + SourceIssue *Issue `json:"source_issue"` + TargetIssue *Issue `json:"target_issue"` +} + +// ListIssueRelations gets a list of related issues of a given issue, +// sorted by the relationship creation datetime (ascending). +// +// Issues will be filtered according to the user authorizations. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/issue_links.html#list-issue-relations +func (s *IssueLinksService) ListIssueRelations(pid interface{}, issueIID int, options ...OptionFunc) ([]*Issue, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/links", url.QueryEscape(project), issueIID) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var is []*Issue + resp, err := s.client.Do(req, &is) + if err != nil { + return nil, resp, err + } + + return is, resp, err +} + +// CreateIssueLinkOptions represents the available CreateIssueLink() options. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/issue_links.html +type CreateIssueLinkOptions struct { + TargetProjectID *string `json:"target_project_id"` + TargetIssueIID *string `json:"target_issue_iid"` +} + +// CreateIssueLink creates a two-way relation between two issues. +// User must be allowed to update both issues in order to succeed. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/issue_links.html#create-an-issue-link +func (s *IssueLinksService) CreateIssueLink(pid interface{}, issueIID int, opt *CreateIssueLinkOptions, options ...OptionFunc) (*IssueLink, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/links", url.QueryEscape(project), issueIID) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + i := new(IssueLink) + resp, err := s.client.Do(req, &i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + +// DeleteIssueLink deletes an issue link, thus removes the two-way relationship. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/issue_links.html#delete-an-issue-link +func (s *IssueLinksService) DeleteIssueLink(pid interface{}, issueIID, issueLinkID int, options ...OptionFunc) (*IssueLink, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/links/%d", + url.QueryEscape(project), + issueIID, + issueLinkID) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, nil, err + } + + i := new(IssueLink) + resp, err := s.client.Do(req, &i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} diff --git a/api/issues.go b/api/issues.go index a7569f0..7574b1d 100644 --- a/api/issues.go +++ b/api/issues.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ package gitlab import ( + "encoding/json" "fmt" - "log" "net/url" "strings" "time" @@ -27,72 +27,159 @@ import ( // IssuesService handles communication with the issue related methods // of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html type IssuesService struct { - client *Client + client *Client + timeStats *timeStatsService +} + +// IssueAuthor represents a author of the issue. +type IssueAuthor struct { + ID int `json:"id"` + State string `json:"state"` + WebURL string `json:"web_url"` + Name string `json:"name"` + AvatarURL string `json:"avatar_url"` + Username string `json:"username"` +} + +// IssueAssignee represents a assignee of the issue. +type IssueAssignee struct { + ID int `json:"id"` + State string `json:"state"` + WebURL string `json:"web_url"` + Name string `json:"name"` + AvatarURL string `json:"avatar_url"` + Username string `json:"username"` +} + +// IssueLinks represents links of the issue. +type IssueLinks struct { + Self string `json:"self"` + Notes string `json:"notes"` + AwardEmoji string `json:"award_emoji"` + Project string `json:"project"` } // Issue represents a GitLab issue. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html type Issue struct { - ID int `json:"id"` - IID int `json:"iid"` - ProjectID int `json:"project_id"` - Title string `json:"title"` - Description string `json:"description"` - Labels []string `json:"labels"` - Milestone struct { - ID int `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - DueDate string `json:"due_date"` - State string `json:"state"` - UpdatedAt time.Time `json:"updated_at"` - CreatedAt time.Time `json:"created_at"` - } `json:"milestone"` - Assignee struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - State string `json:"state"` - CreatedAt time.Time `json:"created_at"` - } `json:"assignee"` - Author struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - State string `json:"state"` - CreatedAt time.Time `json:"created_at"` - } `json:"author"` - State string `json:"state"` - UpdatedAt time.Time `json:"updated_at"` - CreatedAt time.Time `json:"created_at"` + ID int `json:"id"` + IID int `json:"iid"` + ProjectID int `json:"project_id"` + Milestone *Milestone `json:"milestone"` + Author *IssueAuthor `json:"author"` + Description string `json:"description"` + State string `json:"state"` + Assignees []*IssueAssignee `json:"assignees"` + Assignee *IssueAssignee `json:"assignee"` + Upvotes int `json:"upvotes"` + Downvotes int `json:"downvotes"` + Labels []string `json:"labels"` + Title string `json:"title"` + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` + ClosedAt *time.Time `json:"closed_at"` + Subscribed bool `json:"subscribed"` + UserNotesCount int `json:"user_notes_count"` + DueDate *ISOTime `json:"due_date"` + WebURL string `json:"web_url"` + TimeStats *TimeStats `json:"time_stats"` + Confidential bool `json:"confidential"` + Weight int `json:"weight"` + DiscussionLocked bool `json:"discussion_locked"` + Links *IssueLinks `json:"_links"` + IssueLinkID int `json:"issue_link_id"` } func (i Issue) String() string { return Stringify(i) } +// Labels is a custom type with specific marshaling characteristics. +type Labels []string + +// MarshalJSON implements the json.Marshaler interface. +func (l *Labels) MarshalJSON() ([]byte, error) { + return json.Marshal(strings.Join(*l, ",")) +} + // ListIssuesOptions represents the available ListIssues() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html#list-issues +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-issues type ListIssuesOptions struct { ListOptions - State string `url:"state,omitempty" json:"state,omitempty"` - Labels []string `url:"labels,omitempty" json:"labels,omitempty"` - OrderBy string `url:"order_by,omitempty" json:"order_by,omitempty"` - Sort string `url:"sort,omitempty" json:"sort,omitempty"` + State *string `url:"state,omitempty" json:"state,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` + Milestone *string `url:"milestone,omitempty" json:"milestone,omitempty"` + Scope *string `url:"scope,omitempty" json:"scope,omitempty"` + AuthorID *int `url:"author_id,omitempty" json:"author_id,omitempty"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + MyReactionEmoji *string `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"` + IIDs []int `url:"iids[],omitempty" json:"iids,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` + CreatedAfter *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"` + CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"` + UpdatedAfter *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"` + UpdatedBefore *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"` } // ListIssues gets all issues created by authenticated user. This function // takes pagination parameters page and per_page to restrict the list of issues. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html#list-issues -func (s *IssuesService) ListIssues(opt *ListIssuesOptions) ([]*Issue, *Response, error) { - req, err := s.client.NewRequest("GET", "issues", opt) +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-issues +func (s *IssuesService) ListIssues(opt *ListIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) { + req, err := s.client.NewRequest("GET", "issues", opt, options) + if err != nil { + return nil, nil, err + } + + var i []*Issue + resp, err := s.client.Do(req, &i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + +// ListGroupIssuesOptions represents the available ListGroupIssues() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-group-issues +type ListGroupIssuesOptions struct { + ListOptions + State *string `url:"state,omitempty" json:"state,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` + IIDs []int `url:"iids[],omitempty" json:"iids,omitempty"` + Milestone *string `url:"milestone,omitempty" json:"milestone,omitempty"` + Scope *string `url:"scope,omitempty" json:"scope,omitempty"` + AuthorID *int `url:"author_id,omitempty" json:"author_id,omitempty"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + MyReactionEmoji *string `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` + CreatedAfter *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"` + CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"` + UpdatedAfter *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"` + UpdatedBefore *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"` +} + +// ListGroupIssues gets a list of group issues. This function accepts +// pagination parameters page and per_page to return the list of group issues. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-group-issues +func (s *IssuesService) ListGroupIssues(pid interface{}, opt *ListGroupIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) { + group, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/issues", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -108,31 +195,38 @@ func (s *IssuesService) ListIssues(opt *ListIssuesOptions) ([]*Issue, *Response, // ListProjectIssuesOptions represents the available ListProjectIssues() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html#list-issues +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-project-issues type ListProjectIssuesOptions struct { ListOptions - IID int `url:"iid,omitempty" json:"iid,omitempty"` - State string `url:"state,omitempty" json:"state,omitempty"` - Labels []string `url:"labels,omitempty" json:"labels,omitempty"` - Milestone string `url:"milestone,omitempty" json:"milestone,omitempty"` - OrderBy string `url:"order_by,omitempty" json:"order_by,omitempty"` - Sort string `url:"sort,omitempty" json:"sort,omitempty"` + IIDs []int `url:"iids[],omitempty" json:"iids,omitempty"` + State *string `url:"state,omitempty" json:"state,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` + Milestone *string `url:"milestone,omitempty" json:"milestone,omitempty"` + Scope *string `url:"scope,omitempty" json:"scope,omitempty"` + AuthorID *int `url:"author_id,omitempty" json:"author_id,omitempty"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + MyReactionEmoji *string `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` + CreatedAfter *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"` + CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"` + UpdatedAfter *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"` + UpdatedBefore *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"` } // ListProjectIssues gets a list of project issues. This function accepts // pagination parameters page and per_page to return the list of project issues. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html#list-project-issues -func (s *IssuesService) ListProjectIssues( - pid interface{}, - opt *ListProjectIssuesOptions) ([]*Issue, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-project-issues +func (s *IssuesService) ListProjectIssues(pid interface{}, opt *ListProjectIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/issues", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -148,15 +242,15 @@ func (s *IssuesService) ListProjectIssues( // GetIssue gets a single project issue. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html#single-issues -func (s *IssuesService) GetIssue(pid interface{}, issue int) (*Issue, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#single-issues +func (s *IssuesService) GetIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/issues/%d", url.QueryEscape(project), issue) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -172,37 +266,36 @@ func (s *IssuesService) GetIssue(pid interface{}, issue int) (*Issue, *Response, // CreateIssueOptions represents the available CreateIssue() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html#new-issues +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#new-issues type CreateIssueOptions struct { - Title string `url:"title,omitempty" json:"title,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` - AssigneeID int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` - MilestoneID int `url:"milestone_id,omitempty" json:"milestone_id,omitempty"` - Labels []string `url:"labels,omitempty" json:"labels,omitempty"` + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + Confidential *bool `url:"confidential,omitempty" json:"confidential,omitempty"` + AssigneeIDs []int `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"` + MilestoneID *int `url:"milestone_id,omitempty" json:"milestone_id,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` + CreatedAt *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"` + DueDate *ISOTime `url:"due_date,omitempty" json:"due_date,omitempty"` + MergeRequestToResolveDiscussionsOf *int `url:"merge_request_to_resolve_discussions_of,omitempty" json:"merge_request_to_resolve_discussions_of,omitempty"` + DiscussionToResolve *string `url:"discussion_to_resolve,omitempty" json:"discussion_to_resolve,omitempty"` + Weight *int `url:"weight,omitempty" json:"weight,omitempty"` } // CreateIssue creates a new project issue. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html#new-issues -func (s *IssuesService) CreateIssue( - pid interface{}, - opt *CreateIssueOptions) (*Issue, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#new-issues +func (s *IssuesService) CreateIssue(pid interface{}, opt *CreateIssueOptions, options ...OptionFunc) (*Issue, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/issues", url.QueryEscape(project)) - // This is needed to get a single, comma separated string - opt.Labels = []string{strings.Join(opt.Labels, ",")} - - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } - log.Printf("req: %#+v\n", req.URL) - i := new(Issue) resp, err := s.client.Do(req, i) if err != nil { @@ -214,34 +307,78 @@ func (s *IssuesService) CreateIssue( // UpdateIssueOptions represents the available UpdateIssue() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html#edit-issues +// GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#edit-issue type UpdateIssueOptions struct { - Title string `url:"title,omitempty" json:"title,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` - AssigneeID int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` - MilestoneID int `url:"milestone_id,omitempty" json:"milestone_id,omitempty"` - Labels []string `url:"labels,omitempty" json:"labels,omitempty"` - StateEvent string `url:"state_event,omitempty" json:"state_event,omitempty"` + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + Confidential *bool `url:"confidential,omitempty" json:"confidential,omitempty"` + AssigneeIDs []int `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"` + MilestoneID *int `url:"milestone_id,omitempty" json:"milestone_id,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` + StateEvent *string `url:"state_event,omitempty" json:"state_event,omitempty"` + UpdatedAt *time.Time `url:"updated_at,omitempty" json:"updated_at,omitempty"` + DueDate *ISOTime `url:"due_date,omitempty" json:"due_date,omitempty"` + Weight *int `url:"weight,omitempty" json:"weight,omitempty"` + DiscussionLocked *bool `url:"discussion_locked,omitempty" json:"discussion_locked,omitempty"` } // UpdateIssue updates an existing project issue. This function is also used // to mark an issue as closed. // -// GitLab API docs: http://doc.gitlab.com/ce/api/issues.html#edit-issues -func (s *IssuesService) UpdateIssue( - pid interface{}, - issue int, - opt *UpdateIssueOptions) (*Issue, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#edit-issues +func (s *IssuesService) UpdateIssue(pid interface{}, issue int, opt *UpdateIssueOptions, options ...OptionFunc) (*Issue, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/issues/%d", url.QueryEscape(project), issue) - // This is needed to get a single, comma separated string - opt.Labels = []string{strings.Join(opt.Labels, ",")} + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + i := new(Issue) + resp, err := s.client.Do(req, i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + +// DeleteIssue deletes a single project issue. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#delete-an-issue +func (s *IssuesService) DeleteIssue(pid interface{}, issue int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// SubscribeToIssue subscribes the authenticated user to the given issue to +// receive notifications. If the user is already subscribed to the issue, the +// status code 304 is returned. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#subscribe-to-a-merge-request +func (s *IssuesService) SubscribeToIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/subscribe", url.QueryEscape(project), issue) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { return nil, nil, err } @@ -254,3 +391,103 @@ func (s *IssuesService) UpdateIssue( return i, resp, err } + +// UnsubscribeFromIssue unsubscribes the authenticated user from the given +// issue to not receive notifications from that merge request. If the user +// is not subscribed to the issue, status code 304 is returned. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#unsubscribe-from-a-merge-request +func (s *IssuesService) UnsubscribeFromIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/unsubscribe", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + i := new(Issue) + resp, err := s.client.Do(req, i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + +// ListMergeRequestsClosingIssueOptions represents the available +// ListMergeRequestsClosingIssue() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#list-merge-requests-that-will-close-issue-on-merge +type ListMergeRequestsClosingIssueOptions ListOptions + +// ListMergeRequestsClosingIssue gets all the merge requests that will close +// issue when merged. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#list-merge-requests-that-will-close-issue-on-merge +func (s *IssuesService) ListMergeRequestsClosingIssue(pid interface{}, issue int, opt *ListMergeRequestsClosingIssueOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("/projects/%s/issues/%d/closed_by", url.QueryEscape(project), issue) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var m []*MergeRequest + resp, err := s.client.Do(req, &m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// SetTimeEstimate sets the time estimate for a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#set-a-time-estimate-for-an-issue +func (s *IssuesService) SetTimeEstimate(pid interface{}, issue int, opt *SetTimeEstimateOptions, options ...OptionFunc) (*TimeStats, *Response, error) { + return s.timeStats.setTimeEstimate(pid, "issues", issue, opt, options...) +} + +// ResetTimeEstimate resets the time estimate for a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#reset-the-time-estimate-for-an-issue +func (s *IssuesService) ResetTimeEstimate(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) { + return s.timeStats.resetTimeEstimate(pid, "issues", issue, options...) +} + +// AddSpentTime adds spent time for a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#add-spent-time-for-an-issue +func (s *IssuesService) AddSpentTime(pid interface{}, issue int, opt *AddSpentTimeOptions, options ...OptionFunc) (*TimeStats, *Response, error) { + return s.timeStats.addSpentTime(pid, "issues", issue, opt, options...) +} + +// ResetSpentTime resets the spent time for a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#reset-spent-time-for-an-issue +func (s *IssuesService) ResetSpentTime(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) { + return s.timeStats.resetSpentTime(pid, "issues", issue, options...) +} + +// GetTimeSpent gets the spent time for a single project issue. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/issues.html#get-time-tracking-stats +func (s *IssuesService) GetTimeSpent(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) { + return s.timeStats.getTimeSpent(pid, "issues", issue, options...) +} diff --git a/api/issues_test.go b/api/issues_test.go new file mode 100644 index 0000000..98dde53 --- /dev/null +++ b/api/issues_test.go @@ -0,0 +1,404 @@ +package gitlab + +import ( + "fmt" + "log" + "net/http" + "reflect" + "testing" +) + +func TestGetIssue(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":1, "description": "This is test project", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}`) + }) + + issue, _, err := client.Issues.GetIssue("1", 5) + if err != nil { + log.Fatal(err) + } + + want := &Issue{ + ID: 1, + Description: "This is test project", + Author: &IssueAuthor{ID: 1, Name: "snehal"}, + Assignees: []*IssueAssignee{{ID: 1}}, + } + + if !reflect.DeepEqual(want, issue) { + t.Errorf("Issues.GetIssue returned %+v, want %+v", issue, want) + } +} + +func TestDeleteIssue(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + fmt.Fprint(w, `{"id":1, "description": "This is test project", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}`) + }) + + _, err := client.Issues.DeleteIssue("1", 5) + if err != nil { + log.Fatal(err) + } +} + +func TestListIssues(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/issues", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testURL(t, r, "/api/v4/issues?assignee_id=2&author_id=1") + fmt.Fprint(w, `[{"id":1, "description": "This is test project", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}]`) + }) + + listProjectIssue := &ListIssuesOptions{ + AuthorID: Int(01), + AssigneeID: Int(02), + } + + issues, _, err := client.Issues.ListIssues(listProjectIssue) + + if err != nil { + log.Fatal(err) + } + + want := []*Issue{{ + ID: 1, + Description: "This is test project", + Author: &IssueAuthor{ID: 1, Name: "snehal"}, + Assignees: []*IssueAssignee{{ID: 1}}, + }} + + if !reflect.DeepEqual(want, issues) { + t.Errorf("Issues.ListIssues returned %+v, want %+v", issues, want) + } +} + +func TestListProjectIssues(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testURL(t, r, "/api/v4/projects/1/issues?assignee_id=2&author_id=1") + fmt.Fprint(w, `[{"id":1, "description": "This is test project", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}]`) + }) + + listProjectIssue := &ListProjectIssuesOptions{ + AuthorID: Int(01), + AssigneeID: Int(02), + } + issues, _, err := client.Issues.ListProjectIssues("1", listProjectIssue) + if err != nil { + log.Fatal(err) + } + + want := []*Issue{{ + ID: 1, + Description: "This is test project", + Author: &IssueAuthor{ID: 1, Name: "snehal"}, + Assignees: []*IssueAssignee{{ID: 1}}, + }} + + if !reflect.DeepEqual(want, issues) { + t.Errorf("Issues.ListProjectIssues returned %+v, want %+v", issues, want) + } +} + +func TestListGroupIssues(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/groups/1/issues", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testURL(t, r, "/api/v4/groups/1/issues?assignee_id=2&author_id=1&state=Open") + fmt.Fprint(w, `[{"id":1, "description": "This is test project", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}]`) + }) + + listGroupIssue := &ListGroupIssuesOptions{ + State: String("Open"), + AuthorID: Int(01), + AssigneeID: Int(02), + } + + issues, _, err := client.Issues.ListGroupIssues("1", listGroupIssue) + if err != nil { + log.Fatal(err) + } + + want := []*Issue{{ + ID: 1, + Description: "This is test project", + Author: &IssueAuthor{ID: 1, Name: "snehal"}, + Assignees: []*IssueAssignee{{ID: 1}}, + }} + + if !reflect.DeepEqual(want, issues) { + t.Errorf("Issues.ListGroupIssues returned %+v, want %+v", issues, want) + } +} + +func TestCreateIssue(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id":1, "title" : "Title of issue", "description": "This is description of an issue", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}`) + }) + + createIssueOptions := &CreateIssueOptions{ + Title: String("Title of issue"), + Description: String("This is description of an issue"), + } + + issue, _, err := client.Issues.CreateIssue("1", createIssueOptions) + + if err != nil { + log.Fatal(err) + } + + want := &Issue{ + ID: 1, + Title: "Title of issue", + Description: "This is description of an issue", + Author: &IssueAuthor{ID: 1, Name: "snehal"}, + Assignees: []*IssueAssignee{{ID: 1}}, + } + + if !reflect.DeepEqual(want, issue) { + t.Errorf("Issues.CreateIssue returned %+v, want %+v", issue, want) + } +} + +func TestUpdateIssue(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{"id":1, "title" : "Title of issue", "description": "This is description of an issue", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}`) + }) + + updateIssueOpt := &UpdateIssueOptions{ + Title: String("Title of issue"), + Description: String("This is description of an issue"), + } + issue, _, err := client.Issues.UpdateIssue(1, 5, updateIssueOpt) + + if err != nil { + log.Fatal(err) + } + + want := &Issue{ + ID: 1, + Title: "Title of issue", + Description: "This is description of an issue", + Author: &IssueAuthor{ID: 1, Name: "snehal"}, + Assignees: []*IssueAssignee{{ID: 1}}, + } + + if !reflect.DeepEqual(want, issue) { + t.Errorf("Issues.UpdateIssue returned %+v, want %+v", issue, want) + } +} + +func TestSubscribeToIssue(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5/subscribe", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id":1, "title" : "Title of issue", "description": "This is description of an issue", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}`) + }) + + issue, _, err := client.Issues.SubscribeToIssue("1", 5) + + if err != nil { + log.Fatal(err) + } + + want := &Issue{ + ID: 1, + Title: "Title of issue", + Description: "This is description of an issue", + Author: &IssueAuthor{ID: 1, Name: "snehal"}, + Assignees: []*IssueAssignee{{ID: 1}}, + } + + if !reflect.DeepEqual(want, issue) { + t.Errorf("Issues.SubscribeToIssue returned %+v, want %+v", issue, want) + } +} + +func TestUnsubscribeFromIssue(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5/unsubscribe", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id":1, "title" : "Title of issue", "description": "This is description of an issue", "author" : {"id" : 1, "name": "snehal"}, "assignees":[{"id":1}]}`) + }) + + issue, _, err := client.Issues.UnsubscribeFromIssue("1", 5) + if err != nil { + log.Fatal(err) + } + + want := &Issue{ + ID: 1, + Title: "Title of issue", + Description: "This is description of an issue", + Author: &IssueAuthor{ID: 1, Name: "snehal"}, + Assignees: []*IssueAssignee{{ID: 1}}, + } + + if !reflect.DeepEqual(want, issue) { + t.Errorf("Issues.UnsubscribeFromIssue returned %+v, want %+v", issue, want) + } +} + +func TestListMergeRequestsClosingIssue(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5/closed_by", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testURL(t, r, "/api/v4/projects/1/issues/5/closed_by?page=1&per_page=10") + + fmt.Fprint(w, `[{"id":1, "title" : "test merge one"},{"id":2, "title" : "test merge two"}]`) + }) + + listMergeRequestsClosingIssueOpt := &ListMergeRequestsClosingIssueOptions{ + Page: 1, + PerPage: 10, + } + mergeRequest, _, err := client.Issues.ListMergeRequestsClosingIssue("1", 5, listMergeRequestsClosingIssueOpt) + if err != nil { + log.Fatal(err) + } + + want := []*MergeRequest{{ID: 1, Title: "test merge one"}, {ID: 2, Title: "test merge two"}} + + if !reflect.DeepEqual(want, mergeRequest) { + t.Errorf("Issues.ListMergeRequestsClosingIssue returned %+v, want %+v", mergeRequest, want) + } +} + +func TestSetTimeEstimate(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5/time_estimate", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"human_time_estimate": "3h 30m", "human_total_time_spent": null, "time_estimate": 12600, "total_time_spent": 0}`) + }) + + setTimeEstiOpt := &SetTimeEstimateOptions{ + Duration: String("3h 30m"), + } + + timeState, _, err := client.Issues.SetTimeEstimate("1", 5, setTimeEstiOpt) + if err != nil { + log.Fatal(err) + } + want := &TimeStats{HumanTimeEstimate: "3h 30m", HumanTotalTimeSpent: "", TimeEstimate: 12600, TotalTimeSpent: 0} + + if !reflect.DeepEqual(want, timeState) { + t.Errorf("Issues.SetTimeEstimate returned %+v, want %+v", timeState, want) + } +} + +func TestResetTimeEstimate(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5/reset_time_estimate", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"human_time_estimate": null, "human_total_time_spent": null, "time_estimate": 0, "total_time_spent": 0}`) + }) + + timeState, _, err := client.Issues.ResetTimeEstimate("1", 5) + if err != nil { + log.Fatal(err) + } + want := &TimeStats{HumanTimeEstimate: "", HumanTotalTimeSpent: "", TimeEstimate: 0, TotalTimeSpent: 0} + + if !reflect.DeepEqual(want, timeState) { + t.Errorf("Issues.ResetTimeEstimate returned %+v, want %+v", timeState, want) + } +} + +func TestAddSpentTime(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5/add_spent_time", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testURL(t, r, "/api/v4/projects/1/issues/5/add_spent_time") + fmt.Fprint(w, `{"human_time_estimate": null, "human_total_time_spent": "1h", "time_estimate": 0, "total_time_spent": 3600}`) + }) + addSpentTimeOpt := &AddSpentTimeOptions{ + Duration: String("1h"), + } + + timeState, _, err := client.Issues.AddSpentTime("1", 5, addSpentTimeOpt) + if err != nil { + log.Fatal(err) + } + want := &TimeStats{HumanTimeEstimate: "", HumanTotalTimeSpent: "1h", TimeEstimate: 0, TotalTimeSpent: 3600} + + if !reflect.DeepEqual(want, timeState) { + t.Errorf("Issues.AddSpentTime returned %+v, want %+v", timeState, want) + } +} + +func TestResetSpentTime(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5/reset_spent_time", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testURL(t, r, "/api/v4/projects/1/issues/5/reset_spent_time") + fmt.Fprint(w, `{"human_time_estimate": null, "human_total_time_spent": "", "time_estimate": 0, "total_time_spent": 0}`) + }) + + timeState, _, err := client.Issues.ResetSpentTime("1", 5) + if err != nil { + log.Fatal(err) + } + + want := &TimeStats{HumanTimeEstimate: "", HumanTotalTimeSpent: "", TimeEstimate: 0, TotalTimeSpent: 0} + if !reflect.DeepEqual(want, timeState) { + t.Errorf("Issues.ResetSpentTime returned %+v, want %+v", timeState, want) + } +} + +func TestGetTimeSpent(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/issues/5/time_stats", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testURL(t, r, "/api/v4/projects/1/issues/5/time_stats") + fmt.Fprint(w, `{"human_time_estimate": "2h", "human_total_time_spent": "1h", "time_estimate": 7200, "total_time_spent": 3600}`) + }) + + timeState, _, err := client.Issues.GetTimeSpent("1", 5) + if err != nil { + log.Fatal(err) + } + + want := &TimeStats{HumanTimeEstimate: "2h", HumanTotalTimeSpent: "1h", TimeEstimate: 7200, TotalTimeSpent: 3600} + if !reflect.DeepEqual(want, timeState) { + t.Errorf("Issues.GetTimeSpent returned %+v, want %+v", timeState, want) + } +} diff --git a/api/jobs.go b/api/jobs.go new file mode 100644 index 0000000..54e7c86 --- /dev/null +++ b/api/jobs.go @@ -0,0 +1,400 @@ +// +// Copyright 2017, Arkbriar +// +// 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 gitlab + +import ( + "bytes" + "fmt" + "io" + "net/url" + "time" +) + +// JobsService handles communication with the ci builds related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/jobs.html +type JobsService struct { + client *Client +} + +// Job represents a ci build. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/jobs.html +type Job struct { + Commit *Commit `json:"commit"` + CreatedAt *time.Time `json:"created_at"` + Coverage float64 `json:"coverage"` + ArtifactsFile struct { + Filename string `json:"filename"` + Size int `json:"size"` + } `json:"artifacts_file"` + FinishedAt *time.Time `json:"finished_at"` + ID int `json:"id"` + Name string `json:"name"` + Pipeline struct { + ID int `json:"id"` + Ref string `json:"ref"` + Sha string `json:"sha"` + Status string `json:"status"` + } `json:"pipeline"` + Ref string `json:"ref"` + Runner struct { + ID int `json:"id"` + Description string `json:"description"` + Active bool `json:"active"` + IsShared bool `json:"is_shared"` + Name string `json:"name"` + } `json:"runner"` + Stage string `json:"stage"` + StartedAt *time.Time `json:"started_at"` + Status string `json:"status"` + Tag bool `json:"tag"` + User *User `json:"user"` + WebURL string `json:"web_url"` +} + +// ListJobsOptions are options for two list apis +type ListJobsOptions struct { + ListOptions + Scope []BuildStateValue `url:"scope,omitempty" json:"scope,omitempty"` +} + +// ListProjectJobs gets a list of jobs in a project. +// +// The scope of jobs to show, one or array of: created, pending, running, +// failed, success, canceled, skipped; showing all jobs if none provided +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#list-project-jobs +func (s *JobsService) ListProjectJobs(pid interface{}, opts *ListJobsOptions, options ...OptionFunc) ([]Job, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/jobs", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opts, options) + if err != nil { + return nil, nil, err + } + + var jobs []Job + resp, err := s.client.Do(req, &jobs) + if err != nil { + return nil, resp, err + } + + return jobs, resp, err +} + +// ListPipelineJobs gets a list of jobs for specific pipeline in a +// project. If the pipeline ID is not found, it will respond with 404. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#list-pipeline-jobs +func (s *JobsService) ListPipelineJobs(pid interface{}, pipelineID int, opts *ListJobsOptions, options ...OptionFunc) ([]*Job, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipelines/%d/jobs", url.QueryEscape(project), pipelineID) + + req, err := s.client.NewRequest("GET", u, opts, options) + if err != nil { + return nil, nil, err + } + + var jobs []*Job + resp, err := s.client.Do(req, &jobs) + if err != nil { + return nil, resp, err + } + + return jobs, resp, err +} + +// GetJob gets a single job of a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#get-a-single-job +func (s *JobsService) GetJob(pid interface{}, jobID int, options ...OptionFunc) (*Job, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/jobs/%d", url.QueryEscape(project), jobID) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + job := new(Job) + resp, err := s.client.Do(req, job) + if err != nil { + return nil, resp, err + } + + return job, resp, err +} + +// GetJobArtifacts get jobs artifacts of a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#get-job-artifacts +func (s *JobsService) GetJobArtifacts(pid interface{}, jobID int, options ...OptionFunc) (io.Reader, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/jobs/%d/artifacts", url.QueryEscape(project), jobID) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + artifactsBuf := new(bytes.Buffer) + resp, err := s.client.Do(req, artifactsBuf) + if err != nil { + return nil, resp, err + } + + return artifactsBuf, resp, err +} + +// DownloadArtifactsFileOptions represents the available DownloadArtifactsFile() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#download-the-artifacts-file +type DownloadArtifactsFileOptions struct { + Job *string `url:"job" json:"job"` +} + +// DownloadArtifactsFile download the artifacts file from the given +// reference name and job provided the job finished successfully. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#download-the-artifacts-file +func (s *JobsService) DownloadArtifactsFile(pid interface{}, refName string, opt *DownloadArtifactsFileOptions, options ...OptionFunc) (io.Reader, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/jobs/artifacts/%s/download", url.QueryEscape(project), refName) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + artifactsBuf := new(bytes.Buffer) + resp, err := s.client.Do(req, artifactsBuf) + if err != nil { + return nil, resp, err + } + + return artifactsBuf, resp, err +} + +// DownloadSingleArtifactsFile download a file from the artifacts from the +// given reference name and job provided the job finished successfully. +// Only a single file is going to be extracted from the archive and streamed +// to a client. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#download-a-single-artifact-file +func (s *JobsService) DownloadSingleArtifactsFile(pid interface{}, jobID int, artifactPath string, options ...OptionFunc) (io.Reader, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + + u := fmt.Sprintf( + "projects/%s/jobs/%d/artifacts/%s", + url.QueryEscape(project), + jobID, + artifactPath, + ) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + artifactBuf := new(bytes.Buffer) + resp, err := s.client.Do(req, artifactBuf) + if err != nil { + return nil, resp, err + } + + return artifactBuf, resp, err +} + +// GetTraceFile gets a trace of a specific job of a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#get-a-trace-file +func (s *JobsService) GetTraceFile(pid interface{}, jobID int, options ...OptionFunc) (io.Reader, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/jobs/%d/trace", url.QueryEscape(project), jobID) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + traceBuf := new(bytes.Buffer) + resp, err := s.client.Do(req, traceBuf) + if err != nil { + return nil, resp, err + } + + return traceBuf, resp, err +} + +// CancelJob cancels a single job of a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#cancel-a-job +func (s *JobsService) CancelJob(pid interface{}, jobID int, options ...OptionFunc) (*Job, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/jobs/%d/cancel", url.QueryEscape(project), jobID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + job := new(Job) + resp, err := s.client.Do(req, job) + if err != nil { + return nil, resp, err + } + + return job, resp, err +} + +// RetryJob retries a single job of a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#retry-a-job +func (s *JobsService) RetryJob(pid interface{}, jobID int, options ...OptionFunc) (*Job, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/jobs/%d/retry", url.QueryEscape(project), jobID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + job := new(Job) + resp, err := s.client.Do(req, job) + if err != nil { + return nil, resp, err + } + + return job, resp, err +} + +// EraseJob erases a single job of a project, removes a job +// artifacts and a job trace. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#erase-a-job +func (s *JobsService) EraseJob(pid interface{}, jobID int, options ...OptionFunc) (*Job, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/jobs/%d/erase", url.QueryEscape(project), jobID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + job := new(Job) + resp, err := s.client.Do(req, job) + if err != nil { + return nil, resp, err + } + + return job, resp, err +} + +// KeepArtifacts prevents artifacts from being deleted when +// expiration is set. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#keep-artifacts +func (s *JobsService) KeepArtifacts(pid interface{}, jobID int, options ...OptionFunc) (*Job, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/jobs/%d/artifacts/keep", url.QueryEscape(project), jobID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + job := new(Job) + resp, err := s.client.Do(req, job) + if err != nil { + return nil, resp, err + } + + return job, resp, err +} + +// PlayJob triggers a manual action to start a job. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/jobs.html#play-a-job +func (s *JobsService) PlayJob(pid interface{}, jobID int, options ...OptionFunc) (*Job, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/jobs/%d/play", url.QueryEscape(project), jobID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + job := new(Job) + resp, err := s.client.Do(req, job) + if err != nil { + return nil, resp, err + } + + return job, resp, err +} diff --git a/api/jobs_test.go b/api/jobs_test.go new file mode 100644 index 0000000..305af45 --- /dev/null +++ b/api/jobs_test.go @@ -0,0 +1,28 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestListPipelineJobs(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/pipelines/1/jobs", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1},{"id":2}]`) + }) + + jobs, _, err := client.Jobs.ListPipelineJobs(1, 1, nil) + if err != nil { + t.Errorf("Jobs.ListPipelineJobs returned error: %v", err) + } + + want := []*Job{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(want, jobs) { + t.Errorf("Jobs.ListPipelineJobs returned %+v, want %+v", jobs, want) + } +} diff --git a/api/keys.go b/api/keys.go new file mode 100644 index 0000000..dab9b25 --- /dev/null +++ b/api/keys.go @@ -0,0 +1,70 @@ +// +// Copyright 2018, Patrick Webster +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// KeysService handles communication with the +// keys related methods of the GitLab API. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/keys.html +type KeysService struct { + client *Client +} + +// Key represents a GitLab user's SSH key. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/keys.html +type Key struct { + ID int `json:"id"` + Title string `json:"title"` + Key string `json:"key"` + CreatedAt *time.Time `json:"created_at"` + User User `json:"user"` +} + +// GetKeyWithUser gets a single key by id along with the associated +// user information. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/keys.html#get-ssh-key-with-user-by-id-of-an-ssh-key +func (s *KeysService) GetKeyWithUser(kid interface{}, options ...OptionFunc) (*Key, *Response, error) { + key, err := parseID(kid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("keys/%s", url.QueryEscape(key)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + k := new(Key) + resp, err := s.client.Do(req, k) + if err != nil { + return nil, resp, err + } + + return k, resp, err +} diff --git a/api/keys_test.go b/api/keys_test.go new file mode 100644 index 0000000..7854895 --- /dev/null +++ b/api/keys_test.go @@ -0,0 +1,86 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestGetKeyWithUser(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/keys/1", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "id": 1, + "title": "Sample key 25", + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1256k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + "user": { + "id": 25, + "username": "john_smith", + "name": "John Smith", + "email": "john@example.com", + "state": "active", + "bio": null, + "location": null, + "skype": "", + "linkedin": "", + "twitter": "", + "website_url": "http://localhost:3000/john_smith", + "organization": null, + "theme_id": 2, + "color_scheme_id": 1, + "avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon", + "can_create_group": true, + "can_create_project": true, + "projects_limit": 10, + "two_factor_enabled": false, + "identities": [], + "external": false, + "public_email": "john@example.com" + } + }`) + }) + + key, _, err := client.Keys.GetKeyWithUser(1) + if err != nil { + t.Errorf("Keys.GetKeyWithUser returned error: %v", err) + } + + want := &Key{ + ID: 1, + Title: "Sample key 25", + Key: "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1256k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", + User: User{ + ID: 25, + Username: "john_smith", + Email: "john@example.com", + Name: "John Smith", + State: "active", + Bio: "", + Location: "", + Skype: "", + Linkedin: "", + Twitter: "", + WebsiteURL: "http://localhost:3000/john_smith", + Organization: "", + ThemeID: 2, + ColorSchemeID: 1, + AvatarURL: "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon", + CanCreateGroup: true, + CanCreateProject: true, + ProjectsLimit: 10, + TwoFactorEnabled: false, + Identities: []*UserIdentity{}, + External: false, + PublicEmail: "john@example.com", + }, + } + + if !reflect.DeepEqual(want, key) { + t.Errorf("Keys.GetKeyWithUser returned %+v, want %+v", key, want) + } +} diff --git a/api/labels.go b/api/labels.go index c704d4e..5134846 100644 --- a/api/labels.go +++ b/api/labels.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,41 +17,74 @@ package gitlab import ( + "encoding/json" "fmt" "net/url" ) -// LabelsService handles communication with the label related methods -// of the GitLab API. +// LabelsService handles communication with the label related methods of the +// GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/labels.html +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html type LabelsService struct { client *Client } // Label represents a GitLab label. // -// GitLab API docs: http://doc.gitlab.com/ce/api/labels.html +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html type Label struct { - Name string `json:"name"` - Color string `json:"color"` + ID int `json:"id"` + Name string `json:"name"` + Color string `json:"color"` + Description string `json:"description"` + OpenIssuesCount int `json:"open_issues_count"` + ClosedIssuesCount int `json:"closed_issues_count"` + OpenMergeRequestsCount int `json:"open_merge_requests_count"` + Subscribed bool `json:"subscribed"` + Priority int `json:"priority"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (l *Label) UnmarshalJSON(data []byte) error { + type alias Label + if err := json.Unmarshal(data, (*alias)(l)); err != nil { + return err + } + + if l.Name == "" { + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + if title, ok := raw["title"].(string); ok { + l.Name = title + } + } + + return nil } func (l Label) String() string { return Stringify(l) } +// ListLabelsOptions represents the available ListLabels() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#list-labels +type ListLabelsOptions ListOptions + // ListLabels gets all labels for given project. // -// GitLab API docs: http://doc.gitlab.com/ce/api/labels.html#list-labels -func (s *LabelsService) ListLabels(pid interface{}) ([]*Label, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#list-labels +func (s *LabelsService) ListLabels(pid interface{}, opt *ListLabelsOptions, options ...OptionFunc) ([]*Label, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/labels", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -67,26 +100,25 @@ func (s *LabelsService) ListLabels(pid interface{}) ([]*Label, *Response, error) // CreateLabelOptions represents the available CreateLabel() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/labels.html#create-a-new-label +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#create-a-new-label type CreateLabelOptions struct { - Name string `url:"name,omitempty" json:"name,omitempty"` - Color string `url:"color,omitempty" json:"color,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + Color *string `url:"color,omitempty" json:"color,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` } // CreateLabel creates a new label for given repository with given name and // color. // -// GitLab API docs: http://doc.gitlab.com/ce/api/labels.html#create-a-new-label -func (s *LabelsService) CreateLabel( - pid interface{}, - opt *CreateLabelOptions) (*Label, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#create-a-new-label +func (s *LabelsService) CreateLabel(pid interface{}, opt *CreateLabelOptions, options ...OptionFunc) (*Label, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/labels", url.QueryEscape(project)) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -102,57 +134,82 @@ func (s *LabelsService) CreateLabel( // DeleteLabelOptions represents the available DeleteLabel() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/labels.html#delete-a-label +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#delete-a-label type DeleteLabelOptions struct { - Name string `url:"name,omitempty" json:"name,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` } // DeleteLabel deletes a label given by its name. // -// GitLab API docs: http://doc.gitlab.com/ce/api/labels.html#delete-a-label -func (s *LabelsService) DeleteLabel(pid interface{}, opt *DeleteLabelOptions) (*Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#delete-a-label +func (s *LabelsService) DeleteLabel(pid interface{}, opt *DeleteLabelOptions, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } u := fmt.Sprintf("projects/%s/labels", url.QueryEscape(project)) - req, err := s.client.NewRequest("DELETE", u, opt) + req, err := s.client.NewRequest("DELETE", u, opt, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) - if err != nil { - return resp, err - } - - return resp, err + return s.client.Do(req, nil) } // UpdateLabelOptions represents the available UpdateLabel() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/labels.html#delete-a-label +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#delete-a-label type UpdateLabelOptions struct { - Name string `url:"name,omitempty" json:"name,omitempty"` - NewName string `url:"new_name,omitempty" json:"new_name,omitempty"` - Color string `url:"color,omitempty" json:"color,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + NewName *string `url:"new_name,omitempty" json:"new_name,omitempty"` + Color *string `url:"color,omitempty" json:"color,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` } // UpdateLabel updates an existing label with new name or now color. At least // one parameter is required, to update the label. // -// GitLab API docs: http://doc.gitlab.com/ce/api/labels.html#edit-an-existing-label -func (s *LabelsService) UpdateLabel( - pid interface{}, - opt *UpdateLabelOptions) (*Label, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/labels.html#edit-an-existing-label +func (s *LabelsService) UpdateLabel(pid interface{}, opt *UpdateLabelOptions, options ...OptionFunc) (*Label, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/labels", url.QueryEscape(project)) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + l := new(Label) + resp, err := s.client.Do(req, l) + if err != nil { + return nil, resp, err + } + + return l, resp, err +} + +// SubscribeToLabel subscribes the authenticated user to a label to receive +// notifications. If the user is already subscribed to the label, the status +// code 304 is returned. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/labels.html#subscribe-to-a-label +func (s *LabelsService) SubscribeToLabel(pid interface{}, labelID interface{}, options ...OptionFunc) (*Label, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + label, err := parseID(labelID) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/labels/%s/subscribe", url.QueryEscape(project), label) + + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { return nil, nil, err } @@ -165,3 +222,28 @@ func (s *LabelsService) UpdateLabel( return l, resp, err } + +// UnsubscribeFromLabel unsubscribes the authenticated user from a label to not +// receive notifications from it. If the user is not subscribed to the label, the +// status code 304 is returned. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/labels.html#unsubscribe-from-a-label +func (s *LabelsService) UnsubscribeFromLabel(pid interface{}, labelID interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + label, err := parseID(labelID) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/labels/%s/unsubscribe", url.QueryEscape(project), label) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/labels_test.go b/api/labels_test.go new file mode 100644 index 0000000..014b9a2 --- /dev/null +++ b/api/labels_test.go @@ -0,0 +1,144 @@ +package gitlab + +import ( + "fmt" + "log" + "net/http" + "reflect" + "testing" +) + +func TestCreateLabel(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/labels", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id":1, "name": "My Label", "color" : "#11FF22"}`) + }) + + // Create new label + l := &CreateLabelOptions{ + Name: String("My Label"), + Color: String("#11FF22"), + } + label, _, err := client.Labels.CreateLabel("1", l) + + if err != nil { + log.Fatal(err) + } + want := &Label{ID: 1, Name: "My Label", Color: "#11FF22"} + if !reflect.DeepEqual(want, label) { + t.Errorf("Labels.CreateLabel returned %+v, want %+v", label, want) + } +} + +func TestDeleteLabel(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/labels", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + // Delete label + label := &DeleteLabelOptions{ + Name: String("My Label"), + } + + _, err := client.Labels.DeleteLabel("1", label) + + if err != nil { + log.Fatal(err) + } +} + +func TestUpdateLabel(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/labels", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{"id":1, "name": "New Label", "color" : "#11FF23" , "description":"This is updated label"}`) + }) + + // Update label + l := &UpdateLabelOptions{ + Name: String("My Label"), + NewName: String("New Label"), + Color: String("#11FF23"), + Description: String("This is updated label"), + } + + label, resp, err := client.Labels.UpdateLabel("1", l) + + if resp == nil { + log.Fatal(err) + } + if err != nil { + log.Fatal(err) + } + + want := &Label{ID: 1, Name: "New Label", Color: "#11FF23", Description: "This is updated label"} + + if !reflect.DeepEqual(want, label) { + t.Errorf("Labels.UpdateLabel returned %+v, want %+v", label, want) + } +} + +func TestSubscribeToLabel(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/labels/5/subscribe", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{ "id" : 5, "name" : "bug", "color" : "#d9534f", "description": "Bug reported by user", "open_issues_count": 1, "closed_issues_count": 0, "open_merge_requests_count": 1, "subscribed": true,"priority": null}`) + }) + + label, _, err := client.Labels.SubscribeToLabel("1", "5") + if err != nil { + log.Fatal(err) + } + want := &Label{ID: 5, Name: "bug", Color: "#d9534f", Description: "Bug reported by user", OpenIssuesCount: 1, ClosedIssuesCount: 0, OpenMergeRequestsCount: 1, Subscribed: true} + if !reflect.DeepEqual(want, label) { + t.Errorf("Labels.SubscribeToLabel returned %+v, want %+v", label, want) + } +} + +func TestUnsubscribeFromLabel(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/labels/5/unsubscribe", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + }) + + _, err := client.Labels.UnsubscribeFromLabel("1", "5") + if err != nil { + log.Fatal(err) + } +} + +func TestListLabels(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/labels", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{ "id" : 5, "name" : "bug", "color" : "#d9534f", "description": "Bug reported by user", "open_issues_count": 1, "closed_issues_count": 0, "open_merge_requests_count": 1, "subscribed": true,"priority": null}]`) + }) + + o := &ListLabelsOptions{ + Page: 1, + PerPage: 10, + } + label, _, err := client.Labels.ListLabels("1", o) + if err != nil { + t.Log(err.Error() == "invalid ID type 1.1, the ID must be an int or a string") + + } + want := []*Label{{ID: 5, Name: "bug", Color: "#d9534f", Description: "Bug reported by user", OpenIssuesCount: 1, ClosedIssuesCount: 0, OpenMergeRequestsCount: 1, Subscribed: true}} + if !reflect.DeepEqual(want, label) { + t.Errorf("Labels.ListLabels returned %+v, want %+v", label, want) + } +} diff --git a/api/license.go b/api/license.go new file mode 100644 index 0000000..746e99a --- /dev/null +++ b/api/license.go @@ -0,0 +1,94 @@ +// +// Copyright 2018, Patrick Webster +// +// 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 gitlab + +// LicenseService handles communication with the license +// related methods of the GitLab API. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/license.html +type LicenseService struct { + client *Client +} + +// License represents a GitLab license. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/license.html +type License struct { + StartsAt *ISOTime `json:"starts_at"` + ExpiresAt *ISOTime `json:"expires_at"` + Licensee struct { + Name string `json:"Name"` + Company string `json:"Company"` + Email string `json:"Email"` + } `json:"licensee"` + UserLimit int `json:"user_limit"` + ActiveUsers int `json:"active_users"` + AddOns struct { + GitLabFileLocks int `json:"GitLabFileLocks"` + } `json:"add_ons"` +} + +func (l License) String() string { + return Stringify(l) +} + +// GetLicense retrieves information about the current license. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/license.html#retrieve-information-about-the-current-license +func (s *LicenseService) GetLicense() (*License, *Response, error) { + req, err := s.client.NewRequest("GET", "license", nil, nil) + if err != nil { + return nil, nil, err + } + + l := new(License) + resp, err := s.client.Do(req, l) + if err != nil { + return nil, resp, err + } + + return l, resp, err +} + +// AddLicenseOptions represents the available AddLicense() options. +// +// https://docs.gitlab.com/ee/api/license.html#add-a-new-license +type AddLicenseOptions struct { + License *string `url:"license" json:"license"` +} + +// AddLicense adds a new license. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/license.html#add-a-new-license +func (s *LicenseService) AddLicense(opt *AddLicenseOptions, options ...OptionFunc) (*License, *Response, error) { + req, err := s.client.NewRequest("POST", "license", opt, options) + if err != nil { + return nil, nil, err + } + + l := new(License) + resp, err := s.client.Do(req, l) + if err != nil { + return nil, resp, err + } + + return l, resp, err +} diff --git a/api/license_templates.go b/api/license_templates.go new file mode 100644 index 0000000..83f8259 --- /dev/null +++ b/api/license_templates.go @@ -0,0 +1,92 @@ +package gitlab + +import ( + "fmt" +) + +// LicenseTemplate represents a license template. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/licenses.html +type LicenseTemplate struct { + Key string `json:"key"` + Name string `json:"name"` + Nickname string `json:"nickname"` + Featured bool `json:"featured"` + HTMLURL string `json:"html_url"` + SourceURL string `json:"source_url"` + Description string `json:"description"` + Conditions []string `json:"conditions"` + Permissions []string `json:"permissions"` + Limitations []string `json:"limitations"` + Content string `json:"content"` +} + +// LicenseTemplatesService handles communication with the license templates +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/templates/licenses.html +type LicenseTemplatesService struct { + client *Client +} + +// ListLicenseTemplatesOptions represents the available +// ListLicenseTemplates() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/licenses.html#list-license-templates +type ListLicenseTemplatesOptions struct { + ListOptions + Popular *bool `url:"popular,omitempty" json:"popular,omitempty"` +} + +// ListLicenseTemplates get all license templates. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/licenses.html#list-license-templates +func (s *LicenseTemplatesService) ListLicenseTemplates(opt *ListLicenseTemplatesOptions, options ...OptionFunc) ([]*LicenseTemplate, *Response, error) { + req, err := s.client.NewRequest("GET", "templates/licenses", opt, options) + if err != nil { + return nil, nil, err + } + + var lts []*LicenseTemplate + resp, err := s.client.Do(req, <s) + if err != nil { + return nil, resp, err + } + + return lts, resp, err +} + +// GetLicenseTemplateOptions represents the available +// GetLicenseTemplate() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/licenses.html#single-license-template +type GetLicenseTemplateOptions struct { + Project *string `url:"project,omitempty" json:"project,omitempty"` + Fullname *string `url:"fullname,omitempty" json:"fullname,omitempty"` +} + +// GetLicenseTemplate get a single license template. You can pass parameters +// to replace the license placeholder. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/templates/licenses.html#single-license-template +func (s *LicenseTemplatesService) GetLicenseTemplate(template string, opt *GetLicenseTemplateOptions, options ...OptionFunc) (*LicenseTemplate, *Response, error) { + u := fmt.Sprintf("templates/licenses/%s", template) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + lt := new(LicenseTemplate) + resp, err := s.client.Do(req, lt) + if err != nil { + return nil, resp, err + } + + return lt, resp, err +} diff --git a/api/merge_request_approvals.go b/api/merge_request_approvals.go new file mode 100644 index 0000000..85fd882 --- /dev/null +++ b/api/merge_request_approvals.go @@ -0,0 +1,129 @@ +package gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// MergeRequestApprovalsService handles communication with the merge request +// approvals related methods of the GitLab API. This includes reading/updating +// approval settings and approve/unapproving merge requests +// +// GitLab API docs: https://docs.gitlab.com/ee/api/merge_request_approvals.html +type MergeRequestApprovalsService struct { + client *Client +} + +// MergeRequestApprovals represents GitLab merge request approvals. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#merge-request-level-mr-approvals +type MergeRequestApprovals struct { + ID int `json:"id"` + ProjectID int `json:"project_id"` + Title string `json:"title"` + Description string `json:"description"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + MergeStatus string `json:"merge_status"` + ApprovalsBeforeMerge int `json:"approvals_before_merge"` + ApprovalsRequired int `json:"approvals_required"` + ApprovalsLeft int `json:"approvals_left"` + ApprovedBy []*MergeRequestApproverUser `json:"approved_by"` + Approvers []*MergeRequestApproverUser `json:"approvers"` + ApproverGroups []*MergeRequestApproverGroup `json:"approver_groups"` +} + +// MergeRequestApproverGroup represents GitLab project level merge request approver group. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#project-level-mr-approvals +type MergeRequestApproverGroup struct { + Group struct { + ID int `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Description string `json:"description"` + Visibility string `json:"visibility"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + FullName string `json:"full_name"` + FullPath string `json:"full_path"` + LFSEnabled bool `json:"lfs_enabled"` + RequestAccessEnabled bool `json:"request_access_enabled"` + } +} + +// MergeRequestApproverUser represents GitLab project level merge request approver user. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#project-level-mr-approvals +type MergeRequestApproverUser struct { + User struct { + ID int `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + } +} + +func (m MergeRequestApprovals) String() string { + return Stringify(m) +} + +// ApproveMergeRequestOptions represents the available ApproveMergeRequest() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#approve-merge-request +type ApproveMergeRequestOptions struct { + SHA *string `url:"sha,omitempty" json:"sha,omitempty"` +} + +// ApproveMergeRequest approves a merge request on GitLab. If a non-empty sha +// is provided then it must match the sha at the HEAD of the MR. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#approve-merge-request +func (s *MergeRequestApprovalsService) ApproveMergeRequest(pid interface{}, mr int, opt *ApproveMergeRequestOptions, options ...OptionFunc) (*MergeRequestApprovals, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/approve", url.QueryEscape(project), mr) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + m := new(MergeRequestApprovals) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// UnapproveMergeRequest unapproves a previously approved merge request on GitLab. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#unapprove-merge-request +func (s *MergeRequestApprovalsService) UnapproveMergeRequest(pid interface{}, mr int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/unapprove", url.QueryEscape(project), mr) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/merge_requests.go b/api/merge_requests.go index 5609795..2b9dd63 100644 --- a/api/merge_requests.go +++ b/api/merge_requests.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,57 +25,75 @@ import ( // MergeRequestsService handles communication with the merge requests related // methods of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/merge_requests.html +// GitLab API docs: https://docs.gitlab.com/ce/api/merge_requests.html type MergeRequestsService struct { - client *Client + client *Client + timeStats *timeStatsService } // MergeRequest represents a GitLab merge request. // -// GitLab API docs: http://doc.gitlab.com/ce/api/merge_requests.html +// GitLab API docs: https://docs.gitlab.com/ce/api/merge_requests.html type MergeRequest struct { - ID int `json:"id"` - IID int `json:"iid"` - ProjectID int `json:"project_id"` - Title string `json:"title"` - Description string `json:"description"` - WorkInProgress bool `json:"work_in_progress"` - State string `json:"state"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - TargetBranch string `json:"target_branch"` - SourceBranch string `json:"source_branch"` - Upvotes int `json:"upvotes"` - Downvotes int `json:"downvotes"` - Author struct { - Name string `json:"name"` - Username string `json:"username"` - ID int `json:"id"` - State string `json:"state"` - AvatarURL string `json:"avatar_url"` + ID int `json:"id"` + IID int `json:"iid"` + TargetBranch string `json:"target_branch"` + SourceBranch string `json:"source_branch"` + ProjectID int `json:"project_id"` + Title string `json:"title"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + Upvotes int `json:"upvotes"` + Downvotes int `json:"downvotes"` + Author struct { + ID int `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` } `json:"author"` Assignee struct { - Name string `json:"name"` - Username string `json:"username"` - ID int `json:"id"` - State string `json:"state"` - AvatarURL string `json:"avatar_url"` + ID int `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` } `json:"assignee"` - SourceProjectID int `json:"source_project_id"` - TargetProjectID int `json:"target_project_id"` - Labels []string `json:"labels"` - Milestone struct { - ID int `json:"id"` - Iid int `json:"iid"` - ProjectID int `json:"project_id"` - Title string `json:"title"` - Description string `json:"description"` - State string `json:"state"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DueDate string `json:"due_date"` - } `json:"milestone"` - Files []struct { + SourceProjectID int `json:"source_project_id"` + TargetProjectID int `json:"target_project_id"` + Labels []string `json:"labels"` + Description string `json:"description"` + WorkInProgress bool `json:"work_in_progress"` + Milestone *Milestone `json:"milestone"` + MergeWhenPipelineSucceeds bool `json:"merge_when_pipeline_succeeds"` + MergeStatus string `json:"merge_status"` + MergedBy struct { + ID int `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + } `json:"merged_by"` + MergedAt *time.Time `json:"merged_at"` + ClosedBy struct { + ID int `json:"id"` + Username string `json:"username"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + } `json:"closed_by"` + ClosedAt *time.Time `json:"closed_at"` + Subscribed bool `json:"subscribed"` + SHA string `json:"sha"` + MergeCommitSHA string `json:"merge_commit_sha"` + UserNotesCount int `json:"user_notes_count"` + ChangesCount string `json:"changes_count"` + ShouldRemoveSourceBranch bool `json:"should_remove_source_branch"` + ForceRemoveSourceBranch bool `json:"force_remove_source_branch"` + WebURL string `json:"web_url"` + DiscussionLocked bool `json:"discussion_locked"` + Changes []struct { OldPath string `json:"old_path"` NewPath string `json:"new_path"` AMode string `json:"a_mode"` @@ -84,43 +102,190 @@ type MergeRequest struct { NewFile bool `json:"new_file"` RenamedFile bool `json:"renamed_file"` DeletedFile bool `json:"deleted_file"` - } `json:"files"` + } `json:"changes"` + TimeStats *TimeStats `json:"time_stats"` + Squash bool `json:"squash"` + Pipeline struct { + ID int `json:"id"` + Ref string `json:"ref"` + SHA string `json:"sha"` + Status string `json:"status"` + } `json:"pipeline"` + DiffRefs struct { + BaseSha string `json:"base_sha"` + HeadSha string `json:"head_sha"` + StartSha string `json:"start_sha"` + } `json:"diff_refs"` + DivergedCommitsCount int `json:"diverged_commits_count"` + RebaseInProgress bool `json:"rebase_in_progress"` } func (m MergeRequest) String() string { return Stringify(m) } +// MergeRequestDiffVersion represents Gitlab merge request version. +// +// Gitlab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#get-a-single-mr-diff-version +type MergeRequestDiffVersion struct { + ID int `json:"id"` + HeadCommitSHA string `json:"head_commit_sha,omitempty"` + BaseCommitSHA string `json:"base_commit_sha,omitempty"` + StartCommitSHA string `json:"start_commit_sha,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + MergeRequestID int `json:"merge_request_id,omitempty"` + State string `json:"state,omitempty"` + RealSize string `json:"real_size,omitempty"` + Commits []*Commit `json:"commits,omitempty"` + Diffs []*Diff `json:"diffs,omitempty"` +} + +func (m MergeRequestDiffVersion) String() string { + return Stringify(m) +} + // ListMergeRequestsOptions represents the available ListMergeRequests() // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#list-merge-requests +// https://docs.gitlab.com/ce/api/merge_requests.html#list-merge-requests type ListMergeRequestsOptions struct { ListOptions - IID int `url:"iid,omitempty" json:"iid,omitempty"` - State string `url:"state,omitempty" json:"state,omitempty"` - OrderBy string `url:"order_by,omitempty" json:"order_by,omitempty"` - Sort string `url:"sort,omitempty" json:"sort,omitempty"` + State *string `url:"state,omitempty" json:"state,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` + Milestone *string `url:"milestone,omitempty" json:"milestone,omitempty"` + View *string `url:"view,omitempty" json:"view,omitempty"` + Labels Labels `url:"labels,omitempty" json:"labels,omitempty"` + CreatedAfter *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"` + CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"` + UpdatedAfter *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"` + UpdatedBefore *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"` + Scope *string `url:"scope,omitempty" json:"scope,omitempty"` + AuthorID *int `url:"author_id,omitempty" json:"author_id,omitempty"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + MyReactionEmoji *string `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"` + SourceBranch *string `url:"source_branch,omitempty" json:"source_branch,omitempty"` + TargetBranch *string `url:"target_branch,omitempty" json:"target_branch,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` + In *string `url:"in,omitempty" json:"in,omitempty"` + WIP *string `url:"wip,omitempty" json:"wip,omitempty"` +} + +// ListMergeRequests gets all merge requests. The state parameter can be used +// to get only merge requests with a given state (opened, closed, or merged) +// or all of them (all). The pagination parameters page and per_page can be +// used to restrict the list of merge requests. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#list-merge-requests +func (s *MergeRequestsService) ListMergeRequests(opt *ListMergeRequestsOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) { + req, err := s.client.NewRequest("GET", "merge_requests", opt, options) + if err != nil { + return nil, nil, err + } + + var m []*MergeRequest + resp, err := s.client.Do(req, &m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// ListGroupMergeRequestsOptions represents the available ListGroupMergeRequests() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#list-group-merge-requests +type ListGroupMergeRequestsOptions struct { + ListOptions + State *string `url:"state,omitempty" json:"state,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` + Milestone *string `url:"milestone,omitempty" json:"milestone,omitempty"` + View *string `url:"view,omitempty" json:"view,omitempty"` + Labels Labels `url:"labels,omitempty" json:"labels,omitempty"` + CreatedAfter *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"` + CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"` + UpdatedAfter *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"` + UpdatedBefore *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"` + Scope *string `url:"scope,omitempty" json:"scope,omitempty"` + AuthorID *int `url:"author_id,omitempty" json:"author_id,omitempty"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + MyReactionEmoji *string `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"` + SourceBranch *string `url:"source_branch,omitempty" json:"source_branch,omitempty"` + TargetBranch *string `url:"target_branch,omitempty" json:"target_branch,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` +} + +// ListGroupMergeRequests gets all merge requests for this group. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#list-group-merge-requests +func (s *MergeRequestsService) ListGroupMergeRequests(gid interface{}, opt *ListGroupMergeRequestsOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/merge_requests", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var m []*MergeRequest + resp, err := s.client.Do(req, &m) + if err != nil { + return nil, resp, err + } + + return m, resp, err +} + +// ListProjectMergeRequestsOptions represents the available ListMergeRequests() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#list-project-merge-requests +type ListProjectMergeRequestsOptions struct { + ListOptions + IIDs []int `url:"iids[],omitempty" json:"iids,omitempty"` + State *string `url:"state,omitempty" json:"state,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` + Milestone *string `url:"milestone,omitempty" json:"milestone,omitempty"` + View *string `url:"view,omitempty" json:"view,omitempty"` + Labels Labels `url:"labels,omitempty" json:"labels,omitempty"` + CreatedAfter *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"` + CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"` + UpdatedAfter *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"` + UpdatedBefore *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"` + Scope *string `url:"scope,omitempty" json:"scope,omitempty"` + AuthorID *int `url:"author_id,omitempty" json:"author_id,omitempty"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + MyReactionEmoji *string `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"` + SourceBranch *string `url:"source_branch,omitempty" json:"source_branch,omitempty"` + TargetBranch *string `url:"target_branch,omitempty" json:"target_branch,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` + WIP *string `url:"wip,omitempty" json:"wip,omitempty"` } -// ListMergeRequests gets all merge requests for this project. The state -// parameter can be used to get only merge requests with a given state (opened, -// closed, or merged) or all of them (all). The pagination parameters page and -// per_page can be used to restrict the list of merge requests. +// ListProjectMergeRequests gets all merge requests for this project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#list-merge-requests -func (s *MergeRequestsService) ListMergeRequests( - pid interface{}, - opt *ListMergeRequestsOptions) ([]*MergeRequest, *Response, error) { +// https://docs.gitlab.com/ce/api/merge_requests.html#list-project-merge-requests +func (s *MergeRequestsService) ListProjectMergeRequests(pid interface{}, opt *ListProjectMergeRequestsOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/merge_requests", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -134,20 +299,29 @@ func (s *MergeRequestsService) ListMergeRequests( return m, resp, err } +// GetMergeRequestsOptions represents the available GetMergeRequests() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#get-single-mr +type GetMergeRequestsOptions struct { + RenderHTML *bool `url:"render_html,omitempty" json:"render_html,omitempty"` + IncludeDivergedCommitsCount *bool `url:"include_diverged_commits_count,omitempty" json:"include_diverged_commits_count,omitempty"` + IncludeRebaseInProgress *bool `url:"include_rebase_in_progress,omitempty" json:"include_rebase_in_progress,omitempty"` +} + // GetMergeRequest shows information about a single merge request. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#get-single-mr -func (s *MergeRequestsService) GetMergeRequest( - pid interface{}, - mergeRequest int) (*MergeRequest, *Response, error) { +// https://docs.gitlab.com/ce/api/merge_requests.html#get-single-mr +func (s *MergeRequestsService) GetMergeRequest(pid interface{}, mergeRequest int, opt *GetMergeRequestsOptions, options ...OptionFunc) (*MergeRequest, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/merge_request/%d", url.QueryEscape(project), mergeRequest) + u := fmt.Sprintf("projects/%s/merge_requests/%d", url.QueryEscape(project), mergeRequest) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -161,21 +335,76 @@ func (s *MergeRequestsService) GetMergeRequest( return m, resp, err } +// GetMergeRequestApprovals gets information about a merge requests approvals +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#merge-request-level-mr-approvals +func (s *MergeRequestsService) GetMergeRequestApprovals(pid interface{}, mergeRequest int, options ...OptionFunc) (*MergeRequestApprovals, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/approvals", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + a := new(MergeRequestApprovals) + resp, err := s.client.Do(req, a) + if err != nil { + return nil, resp, err + } + + return a, resp, err +} + +// GetMergeRequestCommitsOptions represents the available GetMergeRequestCommits() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#get-single-mr-commits +type GetMergeRequestCommitsOptions ListOptions + +// GetMergeRequestCommits gets a list of merge request commits. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#get-single-mr-commits +func (s *MergeRequestsService) GetMergeRequestCommits(pid interface{}, mergeRequest int, opt *GetMergeRequestCommitsOptions, options ...OptionFunc) ([]*Commit, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/commits", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var c []*Commit + resp, err := s.client.Do(req, &c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} + // GetMergeRequestChanges shows information about the merge request including // its files and changes. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#get-single-mr-changes -func (s *MergeRequestsService) GetMergeRequestChanges( - pid interface{}, - mergeRequest int) (*MergeRequest, *Response, error) { +// https://docs.gitlab.com/ce/api/merge_requests.html#get-single-mr-changes +func (s *MergeRequestsService) GetMergeRequestChanges(pid interface{}, mergeRequest int, options ...OptionFunc) (*MergeRequest, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/merge_request/%d/changes", url.QueryEscape(project), mergeRequest) + u := fmt.Sprintf("projects/%s/merge_requests/%d/changes", url.QueryEscape(project), mergeRequest) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -189,34 +418,95 @@ func (s *MergeRequestsService) GetMergeRequestChanges( return m, resp, err } +// ListMergeRequestPipelines gets all pipelines for the provided merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#list-mr-pipelines +func (s *MergeRequestsService) ListMergeRequestPipelines(pid interface{}, mergeRequest int, options ...OptionFunc) (PipelineList, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/pipelines", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var p PipelineList + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// GetIssuesClosedOnMergeOptions represents the available GetIssuesClosedOnMerge() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#list-issues-that-will-close-on-merge +type GetIssuesClosedOnMergeOptions ListOptions + +// GetIssuesClosedOnMerge gets all the issues that would be closed by merging the +// provided merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#list-issues-that-will-close-on-merge +func (s *MergeRequestsService) GetIssuesClosedOnMerge(pid interface{}, mergeRequest int, opt *GetIssuesClosedOnMergeOptions, options ...OptionFunc) ([]*Issue, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("/projects/%s/merge_requests/%d/closes_issues", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var i []*Issue + resp, err := s.client.Do(req, &i) + if err != nil { + return nil, resp, err + } + + return i, resp, err +} + // CreateMergeRequestOptions represents the available CreateMergeRequest() // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#create-mr +// https://docs.gitlab.com/ce/api/merge_requests.html#create-mr type CreateMergeRequestOptions struct { - Title string `url:"title,omitempty" json:"title,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` - SourceBranch string `url:"source_branch,omitemtpy" json:"source_branch,omitemtpy"` - TargetBranch string `url:"target_branch,omitemtpy" json:"target_branch,omitemtpy"` - AssigneeID int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` - TargetProjectID int `url:"target_project_id,omitempty" json:"target_project_id,omitempty"` + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + SourceBranch *string `url:"source_branch,omitempty" json:"source_branch,omitempty"` + TargetBranch *string `url:"target_branch,omitempty" json:"target_branch,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + TargetProjectID *int `url:"target_project_id,omitempty" json:"target_project_id,omitempty"` + MilestoneID *int `url:"milestone_id,omitempty" json:"milestone_id,omitempty"` + RemoveSourceBranch *bool `url:"remove_source_branch,omitempty" json:"remove_source_branch,omitempty"` + Squash *bool `url:"squash,omitempty" json:"squash,omitempty"` + AllowCollaboration *bool `url:"allow_collaboration,omitempty" json:"allow_collaboration,omitempty"` } // CreateMergeRequest creates a new merge request. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#create-mr -func (s *MergeRequestsService) CreateMergeRequest( - pid interface{}, - opt *CreateMergeRequestOptions) (*MergeRequest, *Response, error) { +// https://docs.gitlab.com/ce/api/merge_requests.html#create-mr +func (s *MergeRequestsService) CreateMergeRequest(pid interface{}, opt *CreateMergeRequestOptions, options ...OptionFunc) (*MergeRequest, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/merge_requests", url.QueryEscape(project)) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -234,30 +524,33 @@ func (s *MergeRequestsService) CreateMergeRequest( // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#update-mr +// https://docs.gitlab.com/ce/api/merge_requests.html#update-mr type UpdateMergeRequestOptions struct { - Title string `url:"title,omitempty" json:"title,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` - TargetBranch string `url:"target_branch,omitemtpy" json:"target_branch,omitemtpy"` - AssigneeID int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` - StateEvent string `url:"state_event,omitempty" json:"state_event,omitempty"` + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + TargetBranch *string `url:"target_branch,omitempty" json:"target_branch,omitempty"` + AssigneeID *int `url:"assignee_id,omitempty" json:"assignee_id,omitempty"` + Labels Labels `url:"labels,comma,omitempty" json:"labels,omitempty"` + MilestoneID *int `url:"milestone_id,omitempty" json:"milestone_id,omitempty"` + StateEvent *string `url:"state_event,omitempty" json:"state_event,omitempty"` + RemoveSourceBranch *bool `url:"remove_source_branch,omitempty" json:"remove_source_branch,omitempty"` + Squash *bool `url:"squash,omitempty" json:"squash,omitempty"` + DiscussionLocked *bool `url:"discussion_locked,omitempty" json:"discussion_locked,omitempty"` + AllowCollaboration *bool `url:"allow_collaboration,omitempty" json:"allow_collaboration,omitempty"` } // UpdateMergeRequest updates an existing project milestone. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#update-mr -func (s *MergeRequestsService) UpdateMergeRequest( - pid interface{}, - mergeRequest int, - opt *UpdateMergeRequestOptions) (*MergeRequest, *Response, error) { +// https://docs.gitlab.com/ce/api/merge_requests.html#update-mr +func (s *MergeRequestsService) UpdateMergeRequest(pid interface{}, mergeRequest int, opt *UpdateMergeRequestOptions, options ...OptionFunc) (*MergeRequest, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/merge_request/%d", url.QueryEscape(project), mergeRequest) + u := fmt.Sprintf("projects/%s/merge_requests/%d", url.QueryEscape(project), mergeRequest) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -271,23 +564,52 @@ func (s *MergeRequestsService) UpdateMergeRequest( return m, resp, err } +// DeleteMergeRequest deletes a merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#delete-a-merge-request +func (s *MergeRequestsService) DeleteMergeRequest(pid interface{}, mergeRequest int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// AcceptMergeRequestOptions represents the available AcceptMergeRequest() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#accept-mr +type AcceptMergeRequestOptions struct { + MergeCommitMessage *string `url:"merge_commit_message,omitempty" json:"merge_commit_message,omitempty"` + ShouldRemoveSourceBranch *bool `url:"should_remove_source_branch,omitempty" json:"should_remove_source_branch,omitempty"` + MergeWhenPipelineSucceeds *bool `url:"merge_when_pipeline_succeeds,omitempty" json:"merge_when_pipeline_succeeds,omitempty"` + SHA *string `url:"sha,omitempty" json:"sha,omitempty"` +} + // AcceptMergeRequest merges changes submitted with MR using this API. If merge // success you get 200 OK. If it has some conflicts and can not be merged - you // get 405 and error message 'Branch cannot be merged'. If merge request is // already merged or closed - you get 405 and error message 'Method Not Allowed' // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#accept-mr -func (s *MergeRequestsService) AcceptMergeRequest( - pid interface{}, - mergeRequest int) (*MergeRequest, *Response, error) { +// https://docs.gitlab.com/ce/api/merge_requests.html#accept-mr +func (s *MergeRequestsService) AcceptMergeRequest(pid interface{}, mergeRequest int, opt *AcceptMergeRequestOptions, options ...OptionFunc) (*MergeRequest, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/merge_request/%d/merge", url.QueryEscape(project), mergeRequest) + u := fmt.Sprintf("projects/%s/merge_requests/%d/merge", url.QueryEscape(project), mergeRequest) - req, err := s.client.NewRequest("PUT", u, nil) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -301,96 +623,231 @@ func (s *MergeRequestsService) AcceptMergeRequest( return m, resp, err } -// MergeRequestComment represents a GitLab merge request comment. +// CancelMergeWhenPipelineSucceeds cancels a merge when pipeline succeeds. If +// you don't have permissions to accept this merge request - you'll get a 401. +// If the merge request is already merged or closed - you get 405 and error +// message 'Method Not Allowed'. In case the merge request is not set to be +// merged when the pipeline succeeds, you'll also get a 406 error. // -// GitLab API docs: http://doc.gitlab.com/ce/api/merge_requests.html -type MergeRequestComment struct { - Note string `json:"note"` - Author struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - State string `json:"state"` - CreatedAt time.Time `json:"created_at"` - } `json:"author"` +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#cancel-merge-when-pipeline-succeeds +func (s *MergeRequestsService) CancelMergeWhenPipelineSucceeds(pid interface{}, mergeRequest int, options ...OptionFunc) (*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/cancel_merge_when_pipeline_succeeds", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("PUT", u, nil, options) + if err != nil { + return nil, nil, err + } + + m := new(MergeRequest) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err } -func (m MergeRequestComment) String() string { - return Stringify(m) +// RebaseMergeRequest automatically rebases the source_branch of the merge +// request against its target_branch. If you don’t have permissions to push +// to the merge request’s source branch, you’ll get a 403 Forbidden response. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#rebase-a-merge-request +func (s *MergeRequestsService) RebaseMergeRequest(pid interface{}, mergeRequest int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/rebase", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("PUT", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) } -// GetMergeRequestCommentsOptions represents the available GetMergeRequestComments() -// options. +// GetMergeRequestDiffVersionsOptions represents the available +// GetMergeRequestDiffVersions() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#get-the-comments-on-a-mr -type GetMergeRequestCommentsOptions struct { - ListOptions +// https://docs.gitlab.com/ce/api/merge_requests.html#get-mr-diff-versions +type GetMergeRequestDiffVersionsOptions ListOptions + +// GetMergeRequestDiffVersions get a list of merge request diff versions. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#get-mr-diff-versions +func (s *MergeRequestsService) GetMergeRequestDiffVersions(pid interface{}, mergeRequest int, opt *GetMergeRequestDiffVersionsOptions, options ...OptionFunc) ([]*MergeRequestDiffVersion, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/versions", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var v []*MergeRequestDiffVersion + resp, err := s.client.Do(req, &v) + if err != nil { + return nil, resp, err + } + + return v, resp, err } -// GetMergeRequestComments gets all the comments associated with a merge -// request. +// GetSingleMergeRequestDiffVersion get a single MR diff version // // GitLab API docs: -// http://doc.gitlab.com/ce/api/merge_requests.html#get-the-comments-on-a-mr -func (s *MergeRequestsService) GetMergeRequestComments( - pid interface{}, - mergeRequest int, - opt *GetMergeRequestCommentsOptions) ([]*MergeRequestComment, *Response, error) { +// https://docs.gitlab.com/ce/api/merge_requests.html#get-a-single-mr-diff-version +func (s *MergeRequestsService) GetSingleMergeRequestDiffVersion(pid interface{}, mergeRequest, version int, options ...OptionFunc) (*MergeRequestDiffVersion, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/merge_request/%d/comments", url.QueryEscape(project), mergeRequest) + u := fmt.Sprintf("projects/%s/merge_requests/%d/versions/%d", url.QueryEscape(project), mergeRequest, version) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } - var c []*MergeRequestComment - resp, err := s.client.Do(req, &c) + var v = new(MergeRequestDiffVersion) + resp, err := s.client.Do(req, v) if err != nil { return nil, resp, err } - return c, resp, err + return v, resp, err } -// PostMergeRequestCommentOptions represents the available -// PostMergeRequestComment() options. +// SubscribeToMergeRequest subscribes the authenticated user to the given merge +// request to receive notifications. If the user is already subscribed to the +// merge request, the status code 304 is returned. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/commits.html#post-comment-to-mr -type PostMergeRequestCommentOptions struct { - Note string `url:"note,omitempty" json:"note,omitempty"` +// https://docs.gitlab.com/ce/api/merge_requests.html#subscribe-to-a-merge-request +func (s *MergeRequestsService) SubscribeToMergeRequest(pid interface{}, mergeRequest int, options ...OptionFunc) (*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/subscribe", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + m := new(MergeRequest) + resp, err := s.client.Do(req, m) + if err != nil { + return nil, resp, err + } + + return m, resp, err } -// PostMergeRequestComment dds a comment to a merge request. +// UnsubscribeFromMergeRequest unsubscribes the authenticated user from the +// given merge request to not receive notifications from that merge request. +// If the user is not subscribed to the merge request, status code 304 is +// returned. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/commits.html#post-comment-to-mr -func (s *MergeRequestsService) PostMergeRequestComment( - pid interface{}, - mergeRequest int, - opt *PostMergeRequestCommentOptions) (*MergeRequestComment, *Response, error) { +// https://docs.gitlab.com/ce/api/merge_requests.html#unsubscribe-from-a-merge-request +func (s *MergeRequestsService) UnsubscribeFromMergeRequest(pid interface{}, mergeRequest int, options ...OptionFunc) (*MergeRequest, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/merge_request/%d/comments", url.QueryEscape(project), mergeRequest) + u := fmt.Sprintf("projects/%s/merge_requests/%d/unsubscribe", url.QueryEscape(project), mergeRequest) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { return nil, nil, err } - c := new(MergeRequestComment) - resp, err := s.client.Do(req, c) + m := new(MergeRequest) + resp, err := s.client.Do(req, m) if err != nil { return nil, resp, err } - return c, resp, err + return m, resp, err +} + +// CreateTodo manually creates a todo for the current user on a merge request. +// If there already exists a todo for the user on that merge request, +// status code 304 is returned. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#create-a-todo +func (s *MergeRequestsService) CreateTodo(pid interface{}, mergeRequest int, options ...OptionFunc) (*Todo, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/todo", url.QueryEscape(project), mergeRequest) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + t := new(Todo) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// SetTimeEstimate sets the time estimate for a single project merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#set-a-time-estimate-for-a-merge-request +func (s *MergeRequestsService) SetTimeEstimate(pid interface{}, mergeRequest int, opt *SetTimeEstimateOptions, options ...OptionFunc) (*TimeStats, *Response, error) { + return s.timeStats.setTimeEstimate(pid, "merge_requests", mergeRequest, opt, options...) +} + +// ResetTimeEstimate resets the time estimate for a single project merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#reset-the-time-estimate-for-a-merge-request +func (s *MergeRequestsService) ResetTimeEstimate(pid interface{}, mergeRequest int, options ...OptionFunc) (*TimeStats, *Response, error) { + return s.timeStats.resetTimeEstimate(pid, "merge_requests", mergeRequest, options...) +} + +// AddSpentTime adds spent time for a single project merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#add-spent-time-for-a-merge-request +func (s *MergeRequestsService) AddSpentTime(pid interface{}, mergeRequest int, opt *AddSpentTimeOptions, options ...OptionFunc) (*TimeStats, *Response, error) { + return s.timeStats.addSpentTime(pid, "merge_requests", mergeRequest, opt, options...) +} + +// ResetSpentTime resets the spent time for a single project merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#reset-spent-time-for-a-merge-request +func (s *MergeRequestsService) ResetSpentTime(pid interface{}, mergeRequest int, options ...OptionFunc) (*TimeStats, *Response, error) { + return s.timeStats.resetSpentTime(pid, "merge_requests", mergeRequest, options...) +} + +// GetTimeSpent gets the spent time for a single project merge request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/merge_requests.html#get-time-tracking-stats +func (s *MergeRequestsService) GetTimeSpent(pid interface{}, mergeRequest int, options ...OptionFunc) (*TimeStats, *Response, error) { + return s.timeStats.getTimeSpent(pid, "merge_requests", mergeRequest, options...) } diff --git a/api/milestones.go b/api/milestones.go index e0a9c0a..c1a69f9 100644 --- a/api/milestones.go +++ b/api/milestones.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,24 +25,25 @@ import ( // MilestonesService handles communication with the milestone related methods // of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/milestones.html +// GitLab API docs: https://docs.gitlab.com/ce/api/milestones.html type MilestonesService struct { client *Client } // Milestone represents a GitLab milestone. // -// GitLab API docs: http://doc.gitlab.com/ce/api/branches.html +// GitLab API docs: https://docs.gitlab.com/ce/api/milestones.html type Milestone struct { - ID int `json:"id"` - Iid int `json:"iid"` - ProjectID int `json:"project_id"` - Title string `json:"title"` - Description string `json:"description"` - DueDate string `json:"due_date"` - State string `json:"state"` - UpdatedAt time.Time `json:"updated_at"` - CreatedAt time.Time `json:"created_at"` + ID int `json:"id"` + IID int `json:"iid"` + ProjectID int `json:"project_id"` + Title string `json:"title"` + Description string `json:"description"` + StartDate *ISOTime `json:"start_date"` + DueDate *ISOTime `json:"due_date"` + State string `json:"state"` + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` } func (m Milestone) String() string { @@ -52,26 +53,26 @@ func (m Milestone) String() string { // ListMilestonesOptions represents the available ListMilestones() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/milestones.html#list-project-milestones +// https://docs.gitlab.com/ce/api/milestones.html#list-project-milestones type ListMilestonesOptions struct { ListOptions - IID int `url:"iid,omitempty" json:"iid,omitempty"` + IIDs []int `url:"iids,omitempty" json:"iids,omitempty"` + State string `url:"state,omitempty" json:"state,omitempty"` + Search string `url:"search,omitempty" json:"search,omitempty"` } // ListMilestones returns a list of project milestones. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/milestones.html#list-project-milestones -func (s *MilestonesService) ListMilestones( - pid interface{}, - opt *ListMilestonesOptions) ([]*Milestone, *Response, error) { +// https://docs.gitlab.com/ce/api/milestones.html#list-project-milestones +func (s *MilestonesService) ListMilestones(pid interface{}, opt *ListMilestonesOptions, options ...OptionFunc) ([]*Milestone, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/milestones", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -88,17 +89,15 @@ func (s *MilestonesService) ListMilestones( // GetMilestone gets a single project milestone. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/milestones.html#get-single-milestone -func (s *MilestonesService) GetMilestone( - pid interface{}, - milestone int) (*Milestone, *Response, error) { +// https://docs.gitlab.com/ce/api/milestones.html#get-single-milestone +func (s *MilestonesService) GetMilestone(pid interface{}, milestone int, options ...OptionFunc) (*Milestone, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/milestones/%d", url.QueryEscape(project), milestone) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -115,27 +114,26 @@ func (s *MilestonesService) GetMilestone( // CreateMilestoneOptions represents the available CreateMilestone() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/milestones.html#create-new-milestone +// https://docs.gitlab.com/ce/api/milestones.html#create-new-milestone type CreateMilestoneOptions struct { - Title string `url:"title,omitempty" json:"title,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` - DueDate string `url:"due_date,omitempty" json:"due_date,omitempty"` + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + StartDate *ISOTime `url:"start_date,omitempty" json:"start_date,omitempty"` + DueDate *ISOTime `url:"due_date,omitempty" json:"due_date,omitempty"` } // CreateMilestone creates a new project milestone. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/milestones.html#create-new-milestone -func (s *MilestonesService) CreateMilestone( - pid interface{}, - opt *CreateMilestoneOptions) (*Milestone, *Response, error) { +// https://docs.gitlab.com/ce/api/milestones.html#create-new-milestone +func (s *MilestonesService) CreateMilestone(pid interface{}, opt *CreateMilestoneOptions, options ...OptionFunc) (*Milestone, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/milestones", url.QueryEscape(project)) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -152,29 +150,27 @@ func (s *MilestonesService) CreateMilestone( // UpdateMilestoneOptions represents the available UpdateMilestone() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/milestones.html#edit-milestone +// https://docs.gitlab.com/ce/api/milestones.html#edit-milestone type UpdateMilestoneOptions struct { - Title string `url:"title,omitempty" json:"title,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` - DueDate string `url:"due_date,omitempty" json:"due_date,omitempty"` - StateEvent string `url:"state_event,omitempty" json:"state_event,omitempty"` + Title *string `url:"title,omitempty" json:"title,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + StartDate *ISOTime `url:"start_date,omitempty" json:"start_date,omitempty"` + DueDate *ISOTime `url:"due_date,omitempty" json:"due_date,omitempty"` + StateEvent *string `url:"state_event,omitempty" json:"state_event,omitempty"` } // UpdateMilestone updates an existing project milestone. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/milestones.html#edit-milestone -func (s *MilestonesService) UpdateMilestone( - pid interface{}, - milestone int, - opt *UpdateMilestoneOptions) (*Milestone, *Response, error) { +// https://docs.gitlab.com/ce/api/milestones.html#edit-milestone +func (s *MilestonesService) UpdateMilestone(pid interface{}, milestone int, opt *UpdateMilestoneOptions, options ...OptionFunc) (*Milestone, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/milestones/%d", url.QueryEscape(project), milestone) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -188,29 +184,42 @@ func (s *MilestonesService) UpdateMilestone( return m, resp, err } -// GetMilestoneIssuesOptions represents the available GetMilestoneIssues() options. +// DeleteMilestone deletes a specified project milestone. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/milestones.html#get-all-issues-assigned-to-a-single-milestone -type GetMilestoneIssuesOptions struct { - ListOptions +// https://docs.gitlab.com/ce/api/milestones.html#delete-project-milestone +func (s *MilestonesService) DeleteMilestone(pid interface{}, milestone int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/milestones/%d", url.QueryEscape(project), milestone) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + return s.client.Do(req, nil) } +// GetMilestoneIssuesOptions represents the available GetMilestoneIssues() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#get-all-issues-assigned-to-a-single-milestone +type GetMilestoneIssuesOptions ListOptions + // GetMilestoneIssues gets all issues assigned to a single project milestone. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/milestones.html#get-all-issues-assigned-to-a-single-milestone -func (s *MilestonesService) GetMilestoneIssues( - pid interface{}, - milestone int, - opt *GetMilestoneIssuesOptions) ([]*Issue, *Response, error) { +// https://docs.gitlab.com/ce/api/milestones.html#get-all-issues-assigned-to-a-single-milestone +func (s *MilestonesService) GetMilestoneIssues(pid interface{}, milestone int, opt *GetMilestoneIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/milestones/%d/issues", url.QueryEscape(project), milestone) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -223,3 +232,36 @@ func (s *MilestonesService) GetMilestoneIssues( return i, resp, err } + +// GetMilestoneMergeRequestsOptions represents the available +// GetMilestoneMergeRequests() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#get-all-merge-requests-assigned-to-a-single-milestone +type GetMilestoneMergeRequestsOptions ListOptions + +// GetMilestoneMergeRequests gets all merge requests assigned to a single +// project milestone. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/milestones.html#get-all-merge-requests-assigned-to-a-single-milestone +func (s *MilestonesService) GetMilestoneMergeRequests(pid interface{}, milestone int, opt *GetMilestoneMergeRequestsOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/milestones/%d/merge_requests", url.QueryEscape(project), milestone) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var mr []*MergeRequest + resp, err := s.client.Do(req, &mr) + if err != nil { + return nil, resp, err + } + + return mr, resp, err +} diff --git a/api/namespaces.go b/api/namespaces.go index 9fc9a9b..9add644 100644 --- a/api/namespaces.go +++ b/api/namespaces.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,21 +16,29 @@ package gitlab +import ( + "fmt" +) + // NamespacesService handles communication with the namespace related methods // of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/namespaces.html +// GitLab API docs: https://docs.gitlab.com/ce/api/namespaces.html type NamespacesService struct { client *Client } // Namespace represents a GitLab namespace. // -// GitLab API docs: http://doc.gitlab.com/ce/api/namespaces.html +// GitLab API docs: https://docs.gitlab.com/ce/api/namespaces.html type Namespace struct { - ID int `json:"id"` - Path string `json:"path"` - Kind string `json:"kind"` + ID int `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Kind string `json:"kind"` + FullPath string `json:"full_path"` + ParentID int `json:"parent_id"` + MembersCountWithDescendants int `json:"members_count_with_descendants"` } func (n Namespace) String() string { @@ -39,17 +47,17 @@ func (n Namespace) String() string { // ListNamespacesOptions represents the available ListNamespaces() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/namespaces.html#list-namespaces +// GitLab API docs: https://docs.gitlab.com/ce/api/namespaces.html#list-namespaces type ListNamespacesOptions struct { ListOptions - Search string `url:"search,omitempty" json:"search,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` } // ListNamespaces gets a list of projects accessible by the authenticated user. // -// GitLab API docs: http://doc.gitlab.com/ce/api/namespaces.html#list-namespaces -func (s *NamespacesService) ListNamespaces(opt *ListNamespacesOptions) ([]*Namespace, *Response, error) { - req, err := s.client.NewRequest("GET", "namespaces", opt) +// GitLab API docs: https://docs.gitlab.com/ce/api/namespaces.html#list-namespaces +func (s *NamespacesService) ListNamespaces(opt *ListNamespacesOptions, options ...OptionFunc) ([]*Namespace, *Response, error) { + req, err := s.client.NewRequest("GET", "namespaces", opt, options) if err != nil { return nil, nil, err } @@ -67,14 +75,14 @@ func (s *NamespacesService) ListNamespaces(opt *ListNamespacesOptions) ([]*Names // or path. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/namespaces.html#search-for-namespace -func (s *NamespacesService) SearchNamespace(query string) ([]*Namespace, *Response, error) { +// https://docs.gitlab.com/ce/api/namespaces.html#search-for-namespace +func (s *NamespacesService) SearchNamespace(query string, options ...OptionFunc) ([]*Namespace, *Response, error) { var q struct { Search string `url:"search,omitempty" json:"search,omitempty"` } q.Search = query - req, err := s.client.NewRequest("GET", "namespaces", &q) + req, err := s.client.NewRequest("GET", "namespaces", &q, options) if err != nil { return nil, nil, err } @@ -87,3 +95,28 @@ func (s *NamespacesService) SearchNamespace(query string) ([]*Namespace, *Respon return n, resp, err } + +// GetNamespace gets a namespace by id. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/namespaces.html#get-namespace-by-id +func (s *NamespacesService) GetNamespace(id interface{}, options ...OptionFunc) (*Namespace, *Response, error) { + namespace, err := parseID(id) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("namespaces/%s", namespace) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + n := new(Namespace) + resp, err := s.client.Do(req, n) + if err != nil { + return nil, resp, err + } + + return n, resp, err +} diff --git a/api/notes.go b/api/notes.go index fa1305a..51c753d 100644 --- a/api/notes.go +++ b/api/notes.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,14 +25,14 @@ import ( // NotesService handles communication with the notes related methods // of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/notes.html +// GitLab API docs: https://docs.gitlab.com/ce/api/notes.html type NotesService struct { client *Client } // Note represents a GitLab note. // -// GitLab API docs: http://doc.gitlab.com/ce/api/notes.html +// GitLab API docs: https://docs.gitlab.com/ce/api/notes.html type Note struct { ID int `json:"id"` Body string `json:"body"` @@ -40,16 +40,49 @@ type Note struct { Title string `json:"title"` FileName string `json:"file_name"` Author struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - State string `json:"state"` - CreatedAt time.Time `json:"created_at"` + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` } `json:"author"` - ExpiresAt *time.Time `json:"expires_at"` - UpdatedAt string `json:"updated_at"` - CreatedAt string `json:"created_at"` + System bool `json:"system"` + ExpiresAt *time.Time `json:"expires_at"` + UpdatedAt string `json:"updated_at"` + CreatedAt string `json:"created_at"` + NoteableID int `json:"noteable_id"` + NoteableType string `json:"noteable_type"` + Position *NotePosition `json:"position"` + Resolvable bool `json:"resolvable"` + Resolved bool `json:"resolved"` + ResolvedBy struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + } `json:"resolved_by"` + NoteableIID int `json:"noteable_iid"` +} + +// NotePosition represents the position attributes of a note. +type NotePosition struct { + BaseSHA string `json:"base_sha"` + StartSHA string `json:"start_sha"` + HeadSHA string `json:"head_sha"` + PositionType string `json:"position_type"` + NewPath string `json:"new_path,omitempty"` + NewLine int `json:"new_line,omitempty"` + OldPath string `json:"old_path,omitempty"` + OldLine int `json:"old_line,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + X int `json:"x,omitempty"` + Y int `json:"y,omitempty"` } func (n Note) String() string { @@ -59,26 +92,25 @@ func (n Note) String() string { // ListIssueNotesOptions represents the available ListIssueNotes() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#list-project-issue-notes +// https://docs.gitlab.com/ce/api/notes.html#list-project-issue-notes type ListIssueNotesOptions struct { ListOptions + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` } // ListIssueNotes gets a list of all notes for a single issue. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#list-project-issue-notes -func (s *NotesService) ListIssueNotes( - pid interface{}, - issue int, - opt *ListIssueNotesOptions) ([]*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#list-project-issue-notes +func (s *NotesService) ListIssueNotes(pid interface{}, issue int, opt *ListIssueNotesOptions, options ...OptionFunc) ([]*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/issues/%d/notes", url.QueryEscape(project), issue) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -95,18 +127,15 @@ func (s *NotesService) ListIssueNotes( // GetIssueNote returns a single note for a specific project issue. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#get-single-issue-note -func (s *NotesService) GetIssueNote( - pid interface{}, - issue int, - note int) (*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#get-single-issue-note +func (s *NotesService) GetIssueNote(pid interface{}, issue, note int, options ...OptionFunc) (*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/issues/%d/notes/%d", url.QueryEscape(project), issue, note) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -124,35 +153,32 @@ func (s *NotesService) GetIssueNote( // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#create-new-issue-note +// https://docs.gitlab.com/ce/api/notes.html#create-new-issue-note type CreateIssueNoteOptions struct { - Body string `url:"body,omitempty" json:"body,omitempty"` + Body *string `url:"body,omitempty" json:"body,omitempty"` } // CreateIssueNoteOptions represents the available CreateIssueNote() // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#create-new-issue-note +// https://docs.gitlab.com/ce/api/notes.html#create-new-issue-note type CreateCommitNoteOptions struct { - Note string `url:"note,omitempty" json:"note,omitempty"` + Note *string `url:"note,omitempty" json:"note,omitempty"` } // CreateIssueNote creates a new note to a single project issue. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#create-new-issue-note -func (s *NotesService) CreateIssueNote( - pid interface{}, - issue int, - opt *CreateIssueNoteOptions) (*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#create-new-issue-note +func (s *NotesService) CreateIssueNote(pid interface{}, issue int, opt *CreateIssueNoteOptions, options ...OptionFunc) (*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/issues/%d/notes", url.QueryEscape(project), issue) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -169,18 +195,15 @@ func (s *NotesService) CreateIssueNote( // CreateIssueNote creates a new note to a single project issue. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#create-new-issue-note -func (s *NotesService) CreateCommitNote( - pid interface{}, - commitID string, - opt *CreateCommitNoteOptions) (*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#create-new-issue-note +func (s *NotesService) CreateCommitNote(pid interface{}, commitID string, opt *CreateCommitNoteOptions, options ...OptionFunc) (*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/commits/%s/comments", url.QueryEscape(project), commitID) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -198,26 +221,22 @@ func (s *NotesService) CreateCommitNote( // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#modify-existing-issue-note +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-issue-note type UpdateIssueNoteOptions struct { - Body string `url:"body,omitempty" json:"body,omitempty"` + Body *string `url:"body,omitempty" json:"body,omitempty"` } // UpdateIssueNote modifies existing note of an issue. // -// http://doc.gitlab.com/ce/api/notes.html#modify-existing-issue-note -func (s *NotesService) UpdateIssueNote( - pid interface{}, - issue int, - note int, - opt *UpdateIssueNoteOptions) (*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-issue-note +func (s *NotesService) UpdateIssueNote(pid interface{}, issue, note int, opt *UpdateIssueNoteOptions, options ...OptionFunc) (*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/issues/%d/notes/%d", url.QueryEscape(project), issue, note) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -231,19 +250,47 @@ func (s *NotesService) UpdateIssueNote( return n, resp, err } +// DeleteIssueNote deletes an existing note of an issue. +// +// https://docs.gitlab.com/ce/api/notes.html#delete-an-issue-note +func (s *NotesService) DeleteIssueNote(pid interface{}, issue, note int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/issues/%d/notes/%d", url.QueryEscape(project), issue, note) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ListSnippetNotesOptions represents the available ListSnippetNotes() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#list-all-snippet-notes +type ListSnippetNotesOptions struct { + ListOptions + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + // ListSnippetNotes gets a list of all notes for a single snippet. Snippet // notes are comments users can post to a snippet. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#list-all-snippet-notes -func (s *NotesService) ListSnippetNotes(pid interface{}, snippet int) ([]*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#list-all-snippet-notes +func (s *NotesService) ListSnippetNotes(pid interface{}, snippet int, opt *ListSnippetNotesOptions, options ...OptionFunc) ([]*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/snippets/%d/notes", url.QueryEscape(project), snippet) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -260,18 +307,15 @@ func (s *NotesService) ListSnippetNotes(pid interface{}, snippet int) ([]*Note, // GetSnippetNote returns a single note for a given snippet. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#get-single-snippet-note -func (s *NotesService) GetSnippetNote( - pid interface{}, - snippet int, - note int) (*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#get-single-snippet-note +func (s *NotesService) GetSnippetNote(pid interface{}, snippet, note int, options ...OptionFunc) (*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/snippets/%d/notes/%d", url.QueryEscape(project), snippet, note) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -289,27 +333,24 @@ func (s *NotesService) GetSnippetNote( // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#create-new-snippet-note +// https://docs.gitlab.com/ce/api/notes.html#create-new-snippet-note type CreateSnippetNoteOptions struct { - Body string `url:"body,omitempty" json:"body,omitempty"` + Body *string `url:"body,omitempty" json:"body,omitempty"` } // CreateSnippetNote creates a new note for a single snippet. Snippet notes are // comments users can post to a snippet. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#create-new-snippet-note -func (s *NotesService) CreateSnippetNote( - pid interface{}, - snippet int, - opt *CreateSnippetNoteOptions) (*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#create-new-snippet-note +func (s *NotesService) CreateSnippetNote(pid interface{}, snippet int, opt *CreateSnippetNoteOptions, options ...OptionFunc) (*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/snippets/%d/notes", url.QueryEscape(project), snippet) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -327,26 +368,22 @@ func (s *NotesService) CreateSnippetNote( // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#modify-existing-snippet-note +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-snippet-note type UpdateSnippetNoteOptions struct { - Body string `url:"body,omitempty" json:"body,omitempty"` + Body *string `url:"body,omitempty" json:"body,omitempty"` } // UpdateSnippetNote modifies existing note of a snippet. // -// http://doc.gitlab.com/ce/api/notes.html#modify-existing-snippet-note -func (s *NotesService) UpdateSnippetNote( - pid interface{}, - snippet int, - note int, - opt *UpdateSnippetNoteOptions) (*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-snippet-note +func (s *NotesService) UpdateSnippetNote(pid interface{}, snippet, note int, opt *UpdateSnippetNoteOptions, options ...OptionFunc) (*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/snippets/%d/notes/%d", url.QueryEscape(project), snippet, note) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -360,20 +397,47 @@ func (s *NotesService) UpdateSnippetNote( return n, resp, err } +// DeleteSnippetNote deletes an existing note of a snippet. +// +// https://docs.gitlab.com/ce/api/notes.html#delete-a-snippet-note +func (s *NotesService) DeleteSnippetNote(pid interface{}, snippet, note int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/snippets/%d/notes/%d", url.QueryEscape(project), snippet, note) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ListMergeRequestNotesOptions represents the available ListMergeRequestNotes() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notes.html#list-all-merge-request-notes +type ListMergeRequestNotesOptions struct { + ListOptions + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + // ListMergeRequestNotes gets a list of all notes for a single merge request. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#list-all-merge-request-notes -func (s *NotesService) ListMergeRequestNotes( - pid interface{}, - mergeRequest int) ([]*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#list-all-merge-request-notes +func (s *NotesService) ListMergeRequestNotes(pid interface{}, mergeRequest int, opt *ListMergeRequestNotesOptions, options ...OptionFunc) ([]*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/merge_requests/%d/notes", url.QueryEscape(project), mergeRequest) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -390,18 +454,15 @@ func (s *NotesService) ListMergeRequestNotes( // GetMergeRequestNote returns a single note for a given merge request. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#get-single-merge-request-note -func (s *NotesService) GetMergeRequestNote( - pid interface{}, - mergeRequest int, - note int) (*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#get-single-merge-request-note +func (s *NotesService) GetMergeRequestNote(pid interface{}, mergeRequest, note int, options ...OptionFunc) (*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/merge_requests/%d/notes/%d", url.QueryEscape(project), mergeRequest, note) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -419,26 +480,23 @@ func (s *NotesService) GetMergeRequestNote( // CreateMergeRequestNote() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#create-new-merge-request-note +// https://docs.gitlab.com/ce/api/notes.html#create-new-merge-request-note type CreateMergeRequestNoteOptions struct { - Body string `url:"body,omitempty" json:"body,omitempty"` + Body *string `url:"body,omitempty" json:"body,omitempty"` } // CreateMergeRequestNote creates a new note for a single merge request. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#create-new-merge-request-note -func (s *NotesService) CreateMergeRequestNote( - pid interface{}, - mergeRequest int, - opt *CreateMergeRequestNoteOptions) (*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#create-new-merge-request-note +func (s *NotesService) CreateMergeRequestNote(pid interface{}, mergeRequest int, opt *CreateMergeRequestNoteOptions, options ...OptionFunc) (*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/merge_requests/%d/notes", url.QueryEscape(project), mergeRequest) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -456,27 +514,22 @@ func (s *NotesService) CreateMergeRequestNote( // UpdateMergeRequestNote() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/notes.html#modify-existing-merge-request-note +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-merge-request-note type UpdateMergeRequestNoteOptions struct { - Body string `url:"body,omitempty" json:"body,omitempty"` + Body *string `url:"body,omitempty" json:"body,omitempty"` } // UpdateMergeRequestNote modifies existing note of a merge request. // -// http://doc.gitlab.com/ce/api/notes.html#modify-existing-merge-request-note -func (s *NotesService) UpdateMergeRequestNote( - pid interface{}, - mergeRequest int, - note int, - opt *UpdateMergeRequestNoteOptions) (*Note, *Response, error) { +// https://docs.gitlab.com/ce/api/notes.html#modify-existing-merge-request-note +func (s *NotesService) UpdateMergeRequestNote(pid interface{}, mergeRequest, note int, opt *UpdateMergeRequestNoteOptions, options ...OptionFunc) (*Note, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf( "projects/%s/merge_requests/%d/notes/%d", url.QueryEscape(project), mergeRequest, note) - - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -489,3 +542,22 @@ func (s *NotesService) UpdateMergeRequestNote( return n, resp, err } + +// DeleteMergeRequestNote deletes an existing note of a merge request. +// +// https://docs.gitlab.com/ce/api/notes.html#delete-a-merge-request-note +func (s *NotesService) DeleteMergeRequestNote(pid interface{}, mergeRequest, note int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf( + "projects/%s/merge_requests/%d/notes/%d", url.QueryEscape(project), mergeRequest, note) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/notifications.go b/api/notifications.go new file mode 100644 index 0000000..a5501dd --- /dev/null +++ b/api/notifications.go @@ -0,0 +1,214 @@ +package gitlab + +import ( + "errors" + "fmt" + "net/url" +) + +// NotificationSettingsService handles communication with the notification settings +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/notification_settings.html +type NotificationSettingsService struct { + client *Client +} + +// NotificationSettings represents the Gitlab notification setting. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#notification-settings +type NotificationSettings struct { + Level NotificationLevelValue `json:"level"` + NotificationEmail string `json:"notification_email"` + Events *NotificationEvents `json:"events"` +} + +// NotificationEvents represents the available notification setting events. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#notification-settings +type NotificationEvents struct { + CloseIssue bool `json:"close_issue"` + CloseMergeRequest bool `json:"close_merge_request"` + FailedPipeline bool `json:"failed_pipeline"` + MergeMergeRequest bool `json:"merge_merge_request"` + NewIssue bool `json:"new_issue"` + NewMergeRequest bool `json:"new_merge_request"` + NewNote bool `json:"new_note"` + ReassignIssue bool `json:"reassign_issue"` + ReassignMergeRequest bool `json:"reassign_merge_request"` + ReopenIssue bool `json:"reopen_issue"` + ReopenMergeRequest bool `json:"reopen_merge_request"` + SuccessPipeline bool `json:"success_pipeline"` +} + +func (ns NotificationSettings) String() string { + return Stringify(ns) +} + +// GetGlobalSettings returns current notification settings and email address. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#global-notification-settings +func (s *NotificationSettingsService) GetGlobalSettings(options ...OptionFunc) (*NotificationSettings, *Response, error) { + u := "notification_settings" + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} + +// NotificationSettingsOptions represents the available options that can be passed +// to the API when updating the notification settings. +type NotificationSettingsOptions struct { + Level *NotificationLevelValue `url:"level,omitempty" json:"level,omitempty"` + NotificationEmail *string `url:"notification_email,omitempty" json:"notification_email,omitempty"` + CloseIssue *bool `url:"close_issue,omitempty" json:"close_issue,omitempty"` + CloseMergeRequest *bool `url:"close_merge_request,omitempty" json:"close_merge_request,omitempty"` + FailedPipeline *bool `url:"failed_pipeline,omitempty" json:"failed_pipeline,omitempty"` + MergeMergeRequest *bool `url:"merge_merge_request,omitempty" json:"merge_merge_request,omitempty"` + NewIssue *bool `url:"new_issue,omitempty" json:"new_issue,omitempty"` + NewMergeRequest *bool `url:"new_merge_request,omitempty" json:"new_merge_request,omitempty"` + NewNote *bool `url:"new_note,omitempty" json:"new_note,omitempty"` + ReassignIssue *bool `url:"reassign_issue,omitempty" json:"reassign_issue,omitempty"` + ReassignMergeRequest *bool `url:"reassign_merge_request,omitempty" json:"reassign_merge_request,omitempty"` + ReopenIssue *bool `url:"reopen_issue,omitempty" json:"reopen_issue,omitempty"` + ReopenMergeRequest *bool `url:"reopen_merge_request,omitempty" json:"reopen_merge_request,omitempty"` + SuccessPipeline *bool `url:"success_pipeline,omitempty" json:"success_pipeline,omitempty"` +} + +// UpdateGlobalSettings updates current notification settings and email address. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#update-global-notification-settings +func (s *NotificationSettingsService) UpdateGlobalSettings(opt *NotificationSettingsOptions, options ...OptionFunc) (*NotificationSettings, *Response, error) { + if opt.Level != nil && *opt.Level == GlobalNotificationLevel { + return nil, nil, errors.New( + "notification level 'global' is not valid for global notification settings") + } + + u := "notification_settings" + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} + +// GetSettingsForGroup returns current group notification settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#group-project-level-notification-settings +func (s *NotificationSettingsService) GetSettingsForGroup(gid interface{}, options ...OptionFunc) (*NotificationSettings, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/notification_settings", url.QueryEscape(group)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} + +// GetSettingsForProject returns current project notification settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#group-project-level-notification-settings +func (s *NotificationSettingsService) GetSettingsForProject(pid interface{}, options ...OptionFunc) (*NotificationSettings, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/notification_settings", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} + +// UpdateSettingsForGroup updates current group notification settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#update-group-project-level-notification-settings +func (s *NotificationSettingsService) UpdateSettingsForGroup(gid interface{}, opt *NotificationSettingsOptions, options ...OptionFunc) (*NotificationSettings, *Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/notification_settings", url.QueryEscape(group)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} + +// UpdateSettingsForProject updates current project notification settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/notification_settings.html#update-group-project-level-notification-settings +func (s *NotificationSettingsService) UpdateSettingsForProject(pid interface{}, opt *NotificationSettingsOptions, options ...OptionFunc) (*NotificationSettings, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/notification_settings", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ns := new(NotificationSettings) + resp, err := s.client.Do(req, ns) + if err != nil { + return nil, resp, err + } + + return ns, resp, err +} diff --git a/api/pages_domains.go b/api/pages_domains.go new file mode 100644 index 0000000..c1db4ea --- /dev/null +++ b/api/pages_domains.go @@ -0,0 +1,194 @@ +package gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// PagesDomainsService handles communication with the pages domains +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pages_domains.html +type PagesDomainsService struct { + client *Client +} + +// PagesDomain represents a pages domain. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pages_domains.html +type PagesDomain struct { + Domain string `json:"domain"` + URL string `json:"url"` + ProjectID int `json:"project_id"` + Verified bool `json:"verified"` + VerificationCode string `json:"verification_code"` + EnabledUntil *time.Time `json:"enabled_until"` + Certificate struct { + Expired bool `json:"expired"` + Expiration *time.Time `json:"expiration"` + } `json:"certificate"` +} + +// ListPagesDomainsOptions represents the available ListPagesDomains() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pages_domains.html#list-pages-domains +type ListPagesDomainsOptions ListOptions + +// ListPagesDomains gets a list of project pages domains. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pages_domains.html#list-pages-domains +func (s *PagesDomainsService) ListPagesDomains(pid interface{}, opt *ListPagesDomainsOptions, options ...OptionFunc) ([]*PagesDomain, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pages/domains", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var pd []*PagesDomain + resp, err := s.client.Do(req, &pd) + if err != nil { + return nil, resp, err + } + + return pd, resp, err +} + +// ListAllPagesDomains gets a list of all pages domains. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pages_domains.html#list-all-pages-domains +func (s *PagesDomainsService) ListAllPagesDomains(options ...OptionFunc) ([]*PagesDomain, *Response, error) { + req, err := s.client.NewRequest("GET", "pages/domains", nil, options) + if err != nil { + return nil, nil, err + } + + var pd []*PagesDomain + resp, err := s.client.Do(req, &pd) + if err != nil { + return nil, resp, err + } + + return pd, resp, err +} + +// GetPagesDomain get a specific pages domain for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pages_domains.html#single-pages-domain +func (s *PagesDomainsService) GetPagesDomain(pid interface{}, domain string, options ...OptionFunc) (*PagesDomain, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pages/domains/%s", url.QueryEscape(project), domain) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + pd := new(PagesDomain) + resp, err := s.client.Do(req, pd) + if err != nil { + return nil, resp, err + } + + return pd, resp, err +} + +// CreatePagesDomainOptions represents the available CreatePagesDomain() options. +// +// GitLab API docs: +// // https://docs.gitlab.com/ce/api/pages_domains.html#create-new-pages-domain +type CreatePagesDomainOptions struct { + Domain *string `url:"domain,omitempty" json:"domain,omitempty"` + Certificate *string `url:"certifiate,omitempty" json:"certifiate,omitempty"` + Key *string `url:"key,omitempty" json:"key,omitempty"` +} + +// CreatePagesDomain creates a new project pages domain. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pages_domains.html#create-new-pages-domain +func (s *PagesDomainsService) CreatePagesDomain(pid interface{}, opt *CreatePagesDomainOptions, options ...OptionFunc) (*PagesDomain, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pages/domains", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + pd := new(PagesDomain) + resp, err := s.client.Do(req, pd) + if err != nil { + return nil, resp, err + } + + return pd, resp, err +} + +// UpdatePagesDomainOptions represents the available UpdatePagesDomain() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pages_domains.html#update-pages-domain +type UpdatePagesDomainOptions struct { + Cerificate *string `url:"certifiate" json:"certifiate"` + Key *string `url:"key" json:"key"` +} + +// UpdatePagesDomain updates an existing project pages domain. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pages_domains.html#update-pages-domain +func (s *PagesDomainsService) UpdatePagesDomain(pid interface{}, domain string, opt *UpdatePagesDomainOptions, options ...OptionFunc) (*PagesDomain, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pages/domains/%s", url.QueryEscape(project), domain) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + pd := new(PagesDomain) + resp, err := s.client.Do(req, pd) + if err != nil { + return nil, resp, err + } + + return pd, resp, err +} + +// DeletePagesDomain deletes an existing prject pages domain. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pages_domains.html#delete-pages-domain +func (s *PagesDomainsService) DeletePagesDomain(pid interface{}, domain string, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/pages/domains/%s", url.QueryEscape(project), domain) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/pipeline_schedules.go b/api/pipeline_schedules.go new file mode 100644 index 0000000..a0906fb --- /dev/null +++ b/api/pipeline_schedules.go @@ -0,0 +1,332 @@ +// +// Copyright 2018, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// PipelineSchedulesService handles communication with the pipeline +// schedules related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipeline_schedules.html +type PipelineSchedulesService struct { + client *Client +} + +// PipelineSchedule represents a pipeline schedule. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html +type PipelineSchedule struct { + ID int `json:"id"` + Description string `json:"description"` + Ref string `json:"ref"` + Cron string `json:"cron"` + CronTimezone string `json:"cron_timezone"` + NextRunAt *time.Time `json:"next_run_at"` + Active bool `json:"active"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + Owner *User `json:"owner"` + LastPipeline struct { + ID int `json:"id"` + SHA string `json:"sha"` + Ref string `json:"ref"` + Status string `json:"status"` + } `json:"last_pipeline"` + Variables []*PipelineVariable `json:"variables"` +} + +// ListPipelineSchedulesOptions represents the available ListPipelineTriggers() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#list-project-triggers +type ListPipelineSchedulesOptions ListOptions + +// ListPipelineSchedules gets a list of project triggers. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html +func (s *PipelineSchedulesService) ListPipelineSchedules(pid interface{}, opt *ListPipelineSchedulesOptions, options ...OptionFunc) ([]*PipelineSchedule, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline_schedules", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var ps []*PipelineSchedule + resp, err := s.client.Do(req, &ps) + if err != nil { + return nil, resp, err + } + + return ps, resp, err +} + +// GetPipelineSchedule gets a pipeline schedule. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html +func (s *PipelineSchedulesService) GetPipelineSchedule(pid interface{}, schedule int, options ...OptionFunc) (*PipelineSchedule, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline_schedules/%d", url.QueryEscape(project), schedule) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(PipelineSchedule) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// CreatePipelineScheduleOptions represents the available +// CreatePipelineSchedule() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#create-a-new-pipeline-schedule +type CreatePipelineScheduleOptions struct { + Description *string `url:"description" json:"description"` + Ref *string `url:"ref" json:"ref"` + Cron *string `url:"cron" json:"cron"` + CronTimezone *string `url:"cron_timezone,omitempty" json:"cron_timezone,omitempty"` + Active *bool `url:"active,omitempty" json:"active,omitempty"` +} + +// CreatePipelineSchedule creates a pipeline schedule. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#create-a-new-pipeline-schedule +func (s *PipelineSchedulesService) CreatePipelineSchedule(pid interface{}, opt *CreatePipelineScheduleOptions, options ...OptionFunc) (*PipelineSchedule, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline_schedules", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + p := new(PipelineSchedule) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// EditPipelineScheduleOptions represents the available +// EditPipelineSchedule() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#create-a-new-pipeline-schedule +type EditPipelineScheduleOptions struct { + Description *string `url:"description,omitempty" json:"description,omitempty"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` + Cron *string `url:"cron,omitempty" json:"cron,omitempty"` + CronTimezone *string `url:"cron_timezone,omitempty" json:"cron_timezone,omitempty"` + Active *bool `url:"active,omitempty" json:"active,omitempty"` +} + +// EditPipelineSchedule edits a pipeline schedule. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#edit-a-pipeline-schedule +func (s *PipelineSchedulesService) EditPipelineSchedule(pid interface{}, schedule int, opt *EditPipelineScheduleOptions, options ...OptionFunc) (*PipelineSchedule, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline_schedules/%d", url.QueryEscape(project), schedule) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + p := new(PipelineSchedule) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// TakeOwnershipOfPipelineSchedule sets the owner of the specified +// pipeline schedule to the user issuing the request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#take-ownership-of-a-pipeline-schedule +func (s *PipelineSchedulesService) TakeOwnershipOfPipelineSchedule(pid interface{}, schedule int, options ...OptionFunc) (*PipelineSchedule, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline_schedules/%d/take_ownership", url.QueryEscape(project), schedule) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(PipelineSchedule) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// DeletePipelineSchedule deletes a pipeline schedule. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#delete-a-pipeline-schedule +func (s *PipelineSchedulesService) DeletePipelineSchedule(pid interface{}, schedule int, options ...OptionFunc) (*PipelineSchedule, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline_schedules/%d", url.QueryEscape(project), schedule) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(PipelineSchedule) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// CreatePipelineScheduleVariableOptions represents the available +// CreatePipelineScheduleVariable() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#create-a-new-pipeline-schedule +type CreatePipelineScheduleVariableOptions struct { + Key *string `url:"key" json:"key"` + Value *string `url:"value" json:"value"` +} + +// CreatePipelineScheduleVariable creates a pipeline schedule variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#create-a-new-pipeline-schedule +func (s *PipelineSchedulesService) CreatePipelineScheduleVariable(pid interface{}, schedule int, opt *CreatePipelineScheduleVariableOptions, options ...OptionFunc) (*PipelineVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline_schedules/%d/variables", url.QueryEscape(project), schedule) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + p := new(PipelineVariable) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// EditPipelineScheduleVariableOptions represents the available +// EditPipelineScheduleVariable() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#edit-a-pipeline-schedule-variable +type EditPipelineScheduleVariableOptions struct { + Value *string `url:"value" json:"value"` +} + +// EditPipelineScheduleVariable creates a pipeline schedule variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#edit-a-pipeline-schedule-variable +func (s *PipelineSchedulesService) EditPipelineScheduleVariable(pid interface{}, schedule int, key string, opt *EditPipelineScheduleVariableOptions, options ...OptionFunc) (*PipelineVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline_schedules/%d/variables/%s", url.QueryEscape(project), schedule, key) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + p := new(PipelineVariable) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// DeletePipelineScheduleVariable creates a pipeline schedule variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_schedules.html#delete-a-pipeline-schedule-variable +func (s *PipelineSchedulesService) DeletePipelineScheduleVariable(pid interface{}, schedule int, key string, options ...OptionFunc) (*PipelineVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline_schedules/%d/variables/%s", url.QueryEscape(project), schedule, key) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(PipelineVariable) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} diff --git a/api/pipeline_triggers.go b/api/pipeline_triggers.go new file mode 100644 index 0000000..6e8dfb8 --- /dev/null +++ b/api/pipeline_triggers.go @@ -0,0 +1,232 @@ +package gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// PipelineTriggersService handles Project pipeline triggers. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html +type PipelineTriggersService struct { + client *Client +} + +// PipelineTrigger represents a project pipeline trigger. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#pipeline-triggers +type PipelineTrigger struct { + ID int `json:"id"` + Description string `json:"description"` + CreatedAt *time.Time `json:"created_at"` + DeletedAt *time.Time `json:"deleted_at"` + LastUsed *time.Time `json:"last_used"` + Token string `json:"token"` + UpdatedAt *time.Time `json:"updated_at"` + Owner *User `json:"owner"` +} + +// ListPipelineTriggersOptions represents the available ListPipelineTriggers() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#list-project-triggers +type ListPipelineTriggersOptions ListOptions + +// ListPipelineTriggers gets a list of project triggers. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#list-project-triggers +func (s *PipelineTriggersService) ListPipelineTriggers(pid interface{}, opt *ListPipelineTriggersOptions, options ...OptionFunc) ([]*PipelineTrigger, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/triggers", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var pt []*PipelineTrigger + resp, err := s.client.Do(req, &pt) + if err != nil { + return nil, resp, err + } + + return pt, resp, err +} + +// GetPipelineTrigger gets a specific pipeline trigger for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#get-trigger-details +func (s *PipelineTriggersService) GetPipelineTrigger(pid interface{}, trigger int, options ...OptionFunc) (*PipelineTrigger, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/triggers/%d", url.QueryEscape(project), trigger) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + pt := new(PipelineTrigger) + resp, err := s.client.Do(req, pt) + if err != nil { + return nil, resp, err + } + + return pt, resp, err +} + +// AddPipelineTriggerOptions represents the available AddPipelineTrigger() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#create-a-project-trigger +type AddPipelineTriggerOptions struct { + Description *string `url:"description,omitempty" json:"description,omitempty"` +} + +// AddPipelineTrigger adds a pipeline trigger to a specified project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#create-a-project-trigger +func (s *PipelineTriggersService) AddPipelineTrigger(pid interface{}, opt *AddPipelineTriggerOptions, options ...OptionFunc) (*PipelineTrigger, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/triggers", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + pt := new(PipelineTrigger) + resp, err := s.client.Do(req, pt) + if err != nil { + return nil, resp, err + } + + return pt, resp, err +} + +// EditPipelineTriggerOptions represents the available EditPipelineTrigger() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#update-a-project-trigger +type EditPipelineTriggerOptions struct { + Description *string `url:"description,omitempty" json:"description,omitempty"` +} + +// EditPipelineTrigger edits a trigger for a specified project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#update-a-project-trigger +func (s *PipelineTriggersService) EditPipelineTrigger(pid interface{}, trigger int, opt *EditPipelineTriggerOptions, options ...OptionFunc) (*PipelineTrigger, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/triggers/%d", url.QueryEscape(project), trigger) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + pt := new(PipelineTrigger) + resp, err := s.client.Do(req, pt) + if err != nil { + return nil, resp, err + } + + return pt, resp, err +} + +// TakeOwnershipOfPipelineTrigger sets the owner of the specified +// pipeline trigger to the user issuing the request. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#take-ownership-of-a-project-trigger +func (s *PipelineTriggersService) TakeOwnershipOfPipelineTrigger(pid interface{}, trigger int, options ...OptionFunc) (*PipelineTrigger, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/triggers/%d/take_ownership", url.QueryEscape(project), trigger) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + pt := new(PipelineTrigger) + resp, err := s.client.Do(req, pt) + if err != nil { + return nil, resp, err + } + + return pt, resp, err +} + +// DeletePipelineTrigger removes a trigger from a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipeline_triggers.html#remove-a-project-trigger +func (s *PipelineTriggersService) DeletePipelineTrigger(pid interface{}, trigger int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/triggers/%d", url.QueryEscape(project), trigger) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// RunPipelineTriggerOptions represents the available RunPipelineTrigger() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/ci/triggers/README.html#triggering-a-pipeline +type RunPipelineTriggerOptions struct { + Ref *string `url:"ref" json:"ref"` + Token *string `url:"token" json:"token"` + Variables map[string]string `url:"variables,omitempty" json:"variables,omitempty"` +} + +// RunPipelineTrigger starts a trigger from a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/ci/triggers/README.html#triggering-a-pipeline +func (s *PipelineTriggersService) RunPipelineTrigger(pid interface{}, opt *RunPipelineTriggerOptions, options ...OptionFunc) (*Pipeline, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/trigger/pipeline", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + pt := new(Pipeline) + resp, err := s.client.Do(req, pt) + if err != nil { + return nil, resp, err + } + + return pt, resp, err +} diff --git a/api/pipeline_triggers_test.go b/api/pipeline_triggers_test.go new file mode 100644 index 0000000..a9236ca --- /dev/null +++ b/api/pipeline_triggers_test.go @@ -0,0 +1,30 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestRunPipeline(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/trigger/pipeline", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id":1, "status":"pending"}`) + }) + + opt := &RunPipelineTriggerOptions{Ref: String("master")} + pipeline, _, err := client.PipelineTriggers.RunPipelineTrigger(1, opt) + + if err != nil { + t.Errorf("PipelineTriggers.RunPipelineTrigger returned error: %v", err) + } + + want := &Pipeline{ID: 1, Status: "pending"} + if !reflect.DeepEqual(want, pipeline) { + t.Errorf("PipelineTriggers.RunPipelineTrigger returned %+v, want %+v", pipeline, want) + } +} diff --git a/api/pipelines.go b/api/pipelines.go new file mode 100644 index 0000000..8466eac --- /dev/null +++ b/api/pipelines.go @@ -0,0 +1,231 @@ +// +// Copyright 2017, Igor Varavko +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// PipelinesService handles communication with the repositories related +// methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html +type PipelinesService struct { + client *Client +} + +// PipelineVariable represents a pipeline variable. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html +type PipelineVariable struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// Pipeline represents a GitLab pipeline. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html +type Pipeline struct { + ID int `json:"id"` + Status string `json:"status"` + Ref string `json:"ref"` + SHA string `json:"sha"` + BeforeSHA string `json:"before_sha"` + Tag bool `json:"tag"` + YamlErrors string `json:"yaml_errors"` + User struct { + Name string `json:"name"` + Username string `json:"username"` + ID int `json:"id"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + } + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at"` + FinishedAt *time.Time `json:"finished_at"` + CommittedAt *time.Time `json:"committed_at"` + Duration int `json:"duration"` + Coverage string `json:"coverage"` + WebURL string `json:"web_url"` +} + +func (i Pipeline) String() string { + return Stringify(i) +} + +// PipelineList represents a GitLab list project pipelines +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#list-project-pipelines +type PipelineList []struct { + ID int `json:"id"` + Status string `json:"status"` + Ref string `json:"ref"` + SHA string `json:"sha"` +} + +func (i PipelineList) String() string { + return Stringify(i) +} + +// ListProjectPipelinesOptions represents the available ListProjectPipelines() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#list-project-pipelines +type ListProjectPipelinesOptions struct { + ListOptions + Scope *string `url:"scope,omitempty" json:"scope,omitempty"` + Status *BuildStateValue `url:"status,omitempty" json:"status,omitempty"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` + SHA *string `url:"sha,omitempty" json:"sha,omitempty"` + YamlErrors *bool `url:"yaml_errors,omitempty" json:"yaml_errors,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + Username *string `url:"username,omitempty" json:"username,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + +// ListProjectPipelines gets a list of project piplines. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#list-project-pipelines +func (s *PipelinesService) ListProjectPipelines(pid interface{}, opt *ListProjectPipelinesOptions, options ...OptionFunc) (PipelineList, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipelines", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var p PipelineList + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + return p, resp, err +} + +// GetPipeline gets a single project pipeline. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#get-a-single-pipeline +func (s *PipelinesService) GetPipeline(pid interface{}, pipeline int, options ...OptionFunc) (*Pipeline, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipelines/%d", url.QueryEscape(project), pipeline) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(Pipeline) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// CreatePipelineOptions represents the available CreatePipeline() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#create-a-new-pipeline +type CreatePipelineOptions struct { + Ref *string `url:"ref" json:"ref"` + Variables []*PipelineVariable `url:"variables,omitempty" json:"variables,omitempty"` +} + +// CreatePipeline creates a new project pipeline. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/pipelines.html#create-a-new-pipeline +func (s *PipelinesService) CreatePipeline(pid interface{}, opt *CreatePipelineOptions, options ...OptionFunc) (*Pipeline, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipeline", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + p := new(Pipeline) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// RetryPipelineBuild retries failed builds in a pipeline +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/pipelines.html#retry-failed-builds-in-a-pipeline +func (s *PipelinesService) RetryPipelineBuild(pid interface{}, pipelineID int, options ...OptionFunc) (*Pipeline, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipelines/%d/retry", project, pipelineID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(Pipeline) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// CancelPipelineBuild cancels a pipeline builds +// +// GitLab API docs: +//https://docs.gitlab.com/ce/api/pipelines.html#cancel-a-pipelines-builds +func (s *PipelinesService) CancelPipelineBuild(pid interface{}, pipelineID int, options ...OptionFunc) (*Pipeline, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/pipelines/%d/cancel", project, pipelineID) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(Pipeline) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} diff --git a/api/pipelines_test.go b/api/pipelines_test.go new file mode 100644 index 0000000..a34a84b --- /dev/null +++ b/api/pipelines_test.go @@ -0,0 +1,111 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestListProjectPipelines(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/pipelines", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1},{"id":2}]`) + }) + + opt := &ListProjectPipelinesOptions{Ref: String("master")} + piplines, _, err := client.Pipelines.ListProjectPipelines(1, opt) + if err != nil { + t.Errorf("Pipelines.ListProjectPipelines returned error: %v", err) + } + + want := PipelineList{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(want, piplines) { + t.Errorf("Pipelines.ListProjectPipelines returned %+v, want %+v", piplines, want) + } +} + +func TestGetPipeline(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/pipelines/5949167", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":1,"status":"success"}`) + }) + + pipeline, _, err := client.Pipelines.GetPipeline(1, 5949167) + if err != nil { + t.Errorf("Pipelines.GetPipeline returned error: %v", err) + } + + want := &Pipeline{ID: 1, Status: "success"} + if !reflect.DeepEqual(want, pipeline) { + t.Errorf("Pipelines.GetPipeline returned %+v, want %+v", pipeline, want) + } +} + +func TestCreatePipeline(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/pipeline", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id":1, "status":"pending"}`) + }) + + opt := &CreatePipelineOptions{Ref: String("master")} + pipeline, _, err := client.Pipelines.CreatePipeline(1, opt) + + if err != nil { + t.Errorf("Pipelines.CreatePipeline returned error: %v", err) + } + + want := &Pipeline{ID: 1, Status: "pending"} + if !reflect.DeepEqual(want, pipeline) { + t.Errorf("Pipelines.CreatePipeline returned %+v, want %+v", pipeline, want) + } +} + +func TestRetryPipelineBuild(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/pipelines/5949167/retry", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprintln(w, `{"id":1, "status":"pending"}`) + }) + + pipeline, _, err := client.Pipelines.RetryPipelineBuild(1, 5949167) + if err != nil { + t.Errorf("Pipelines.RetryPipelineBuild returned error: %v", err) + } + + want := &Pipeline{ID: 1, Status: "pending"} + if !reflect.DeepEqual(want, pipeline) { + t.Errorf("Pipelines.RetryPipelineBuild returned %+v, want %+v", pipeline, want) + } +} + +func TestCancelPipelineBuild(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/pipelines/5949167/cancel", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprintln(w, `{"id":1, "status":"canceled"}`) + }) + + pipeline, _, err := client.Pipelines.CancelPipelineBuild(1, 5949167) + if err != nil { + t.Errorf("Pipelines.CancelPipelineBuild returned error: %v", err) + } + + want := &Pipeline{ID: 1, Status: "canceled"} + if !reflect.DeepEqual(want, pipeline) { + t.Errorf("Pipelines.CancelPipelineBuild returned %+v, want %+v", pipeline, want) + } +} diff --git a/api/project_badges.go b/api/project_badges.go new file mode 100644 index 0000000..0c39838 --- /dev/null +++ b/api/project_badges.go @@ -0,0 +1,208 @@ +package gitlab + +import ( + "fmt" + "net/url" +) + +// ProjectBadge represents a project badge. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#list-all-badges-of-a-project +type ProjectBadge struct { + ID int `json:"id"` + LinkURL string `json:"link_url"` + ImageURL string `json:"image_url"` + RenderedLinkURL string `json:"rendered_link_url"` + RenderedImageURL string `json:"rendered_image_url"` + // Kind represents a project badge kind. Can be empty, when used PreviewProjectBadge(). + Kind string `json:"kind"` +} + +// ProjectBadgesService handles communication with the project badges +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/project_badges.html +type ProjectBadgesService struct { + client *Client +} + +// ListProjectBadgesOptions represents the available ListProjectBadges() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#list-all-badges-of-a-project +type ListProjectBadgesOptions ListOptions + +// ListProjectBadges gets a list of a project's badges and its group badges. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#list-all-badges-of-a-project +func (s *ProjectBadgesService) ListProjectBadges(pid interface{}, opt *ListProjectBadgesOptions, options ...OptionFunc) ([]*ProjectBadge, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/badges", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var pb []*ProjectBadge + resp, err := s.client.Do(req, &pb) + if err != nil { + return nil, resp, err + } + + return pb, resp, err +} + +// GetProjectBadge gets a project badge. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#get-a-badge-of-a-project +func (s *ProjectBadgesService) GetProjectBadge(pid interface{}, badge int, options ...OptionFunc) (*ProjectBadge, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/badges/%d", url.QueryEscape(project), badge) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + pb := new(ProjectBadge) + resp, err := s.client.Do(req, pb) + if err != nil { + return nil, resp, err + } + + return pb, resp, err +} + +// AddProjectBadgeOptions represents the available AddProjectBadge() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#add-a-badge-to-a-project +type AddProjectBadgeOptions struct { + LinkURL *string `url:"link_url,omitempty" json:"link_url,omitempty"` + ImageURL *string `url:"image_url,omitempty" json:"image_url,omitempty"` +} + +// AddProjectBadge adds a badge to a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#add-a-badge-to-a-project +func (s *ProjectBadgesService) AddProjectBadge(pid interface{}, opt *AddProjectBadgeOptions, options ...OptionFunc) (*ProjectBadge, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/badges", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + pb := new(ProjectBadge) + resp, err := s.client.Do(req, pb) + if err != nil { + return nil, resp, err + } + + return pb, resp, err +} + +// EditProjectBadgeOptions represents the available EditProjectBadge() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#edit-a-badge-of-a-project +type EditProjectBadgeOptions struct { + LinkURL *string `url:"link_url,omitempty" json:"link_url,omitempty"` + ImageURL *string `url:"image_url,omitempty" json:"image_url,omitempty"` +} + +// EditProjectBadge updates a badge of a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#edit-a-badge-of-a-project +func (s *ProjectBadgesService) EditProjectBadge(pid interface{}, badge int, opt *EditProjectBadgeOptions, options ...OptionFunc) (*ProjectBadge, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/badges/%d", url.QueryEscape(project), badge) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + pb := new(ProjectBadge) + resp, err := s.client.Do(req, pb) + if err != nil { + return nil, resp, err + } + + return pb, resp, err +} + +// DeleteProjectBadge removes a badge from a project. Only project's +// badges will be removed by using this endpoint. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#remove-a-badge-from-a-project +func (s *ProjectBadgesService) DeleteProjectBadge(pid interface{}, badge int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/badges/%d", url.QueryEscape(project), badge) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ProjectBadgePreviewOptions represents the available PreviewProjectBadge() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#preview-a-badge-from-a-project +type ProjectBadgePreviewOptions struct { + LinkURL *string `url:"link_url,omitempty" json:"link_url,omitempty"` + ImageURL *string `url:"image_url,omitempty" json:"image_url,omitempty"` +} + +// PreviewProjectBadge returns how the link_url and image_url final URLs would be after +// resolving the placeholder interpolation. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_badges.html#preview-a-badge-from-a-project +func (s *ProjectBadgesService) PreviewProjectBadge(pid interface{}, opt *ProjectBadgePreviewOptions, options ...OptionFunc) (*ProjectBadge, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/badges/render", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + pb := new(ProjectBadge) + resp, err := s.client.Do(req, &pb) + if err != nil { + return nil, resp, err + } + + return pb, resp, err +} diff --git a/api/project_clusters.go b/api/project_clusters.go new file mode 100644 index 0000000..a139efb --- /dev/null +++ b/api/project_clusters.go @@ -0,0 +1,218 @@ +// +// Copyright 2019, Matej Velikonja +// +// 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 gitlab + +import ( + "fmt" + "net/url" + "time" +) + +// ProjectClustersService handles communication with the +// project clusters related methods of the GitLab API. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_clusters.html +type ProjectClustersService struct { + client *Client +} + +// ProjectCluster represents a GitLab Project Cluster. +// +// GitLab API docs: https://docs.gitlab.com/ee/api/project_clusters.html +type ProjectCluster struct { + ID int `json:"id"` + Name string `json:"name"` + CreatedAt *time.Time `json:"created_at"` + ProviderType string `json:"provider_type"` + PlatformType string `json:"platform_type"` + EnvironmentScope string `json:"environment_scope"` + ClusterType string `json:"cluster_type"` + User *User `json:"user"` + PlatformKubernetes *PlatformKubernetes `json:"platform_kubernetes"` + Project *Project `json:"project"` +} + +func (v ProjectCluster) String() string { + return Stringify(v) +} + +// PlatformKubernetes represents a GitLab Project Cluster PlatformKubernetes. +type PlatformKubernetes struct { + APIURL string `json:"api_url"` + Token string `json:"token"` + CaCert string `json:"ca_cert"` + Namespace string `json:"namespace"` + AuthorizationType string `json:"authorization_type"` +} + +// ListClusters gets a list of all clusters in a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_clusters.html#list-project-clusters +func (s *ProjectClustersService) ListClusters(pid interface{}, options ...OptionFunc) ([]*ProjectCluster, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/clusters", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var pcs []*ProjectCluster + resp, err := s.client.Do(req, &pcs) + if err != nil { + return nil, resp, err + } + + return pcs, resp, err +} + +// GetCluster gets a cluster. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_clusters.html#get-a-single-project-cluster +func (s *ProjectClustersService) GetCluster(pid interface{}, cluster int, options ...OptionFunc) (*ProjectCluster, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/clusters/%d", url.QueryEscape(project), cluster) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + pc := new(ProjectCluster) + resp, err := s.client.Do(req, &pc) + if err != nil { + return nil, resp, err + } + + return pc, resp, err +} + +// AddClusterOptions represents the available AddCluster() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_clusters.html#add-existing-cluster-to-project +type AddClusterOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + Enabled *bool `url:"enabled,omitempty" json:"enabled,omitempty"` + EnvironmentScope *string `url:"environment_scope,omitempty" json:"environment_scope,omitempty"` + PlatformKubernetes *AddPlatformKubernetesOptions `url:"platform_kubernetes_attributes,omitempty" json:"platform_kubernetes_attributes,omitempty"` +} + +// AddPlatformKubernetesOptions represents the available PlatformKubernetes options for adding. +type AddPlatformKubernetesOptions struct { + APIURL *string `url:"api_url,omitempty" json:"api_url,omitempty"` + Token *string `url:"token,omitempty" json:"token,omitempty"` + CaCert *string `url:"ca_cert,omitempty" json:"ca_cert,omitempty"` + Namespace *string `url:"namespace,omitempty" json:"namespace,omitempty"` + AuthorizationType *string `url:"authorization_type,omitempty" json:"authorization_type,omitempty"` +} + +// AddCluster adds an existing cluster to the project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_clusters.html#add-existing-cluster-to-project +func (s *ProjectClustersService) AddCluster(pid interface{}, opt *AddClusterOptions, options ...OptionFunc) (*ProjectCluster, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/clusters/user", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + pc := new(ProjectCluster) + resp, err := s.client.Do(req, pc) + if err != nil { + return nil, resp, err + } + + return pc, resp, err +} + +// EditClusterOptions represents the available EditCluster() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_clusters.html#edit-project-cluster +type EditClusterOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + EnvironmentScope *string `url:"environment_scope,omitempty" json:"environment_scope,omitempty"` + PlatformKubernetes *EditPlatformKubernetesOptions `url:"platform_kubernetes_attributes,omitempty" json:"platform_kubernetes_attributes,omitempty"` +} + +// EditPlatformKubernetesOptions represents the available PlatformKubernetes options for editing. +type EditPlatformKubernetesOptions struct { + APIURL *string `url:"api_url,omitempty" json:"api_url,omitempty"` + Token *string `url:"token,omitempty" json:"token,omitempty"` + CaCert *string `url:"ca_cert,omitempty" json:"ca_cert,omitempty"` + Namespace *string `url:"namespace,omitempty" json:"namespace,omitempty"` +} + +// EditCluster updates an existing project cluster. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_clusters.html#edit-project-cluster +func (s *ProjectClustersService) EditCluster(pid interface{}, cluster int, opt *EditClusterOptions, options ...OptionFunc) (*ProjectCluster, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/clusters/%d", url.QueryEscape(project), cluster) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + pc := new(ProjectCluster) + resp, err := s.client.Do(req, pc) + if err != nil { + return nil, resp, err + } + + return pc, resp, err +} + +// DeleteCluster deletes an existing project cluster. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_clusters.html#delete-project-cluster +func (s *ProjectClustersService) DeleteCluster(pid interface{}, cluster int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/clusters/%d", url.QueryEscape(project), cluster) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/project_clusters_test.go b/api/project_clusters_test.go new file mode 100644 index 0000000..029e411 --- /dev/null +++ b/api/project_clusters_test.go @@ -0,0 +1,305 @@ +package gitlab + +import ( + "fmt" + "net/http" + "testing" +) + +func TestListClusters(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + pid := 1234 + + mux.HandleFunc("/api/v4/projects/1234/clusters", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + response := `[ + { + "id":18, + "name":"cluster-1", + "created_at":"2019-01-02T20:18:12.563Z", + "provider_type":"user", + "platform_type":"kubernetes", + "environment_scope":"*", + "cluster_type":"project_type", + "user": + { + "id":1, + "name":"Administrator", + "username":"root", + "state":"active", + "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..", + "web_url":"https://gitlab.example.com/root" + }, + "platform_kubernetes": + { + "api_url":"https://104.197.68.152", + "namespace":"cluster-1-namespace", + "authorization_type":"rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----" + } + } +]` + fmt.Fprint(w, response) + }) + + clusters, _, err := client.ProjectCluster.ListClusters(pid) + + if err != nil { + t.Errorf("ProjectClusters.ListClusters returned error: %v", err) + } + + if len(clusters) != 1 { + t.Errorf("expected 1 cluster; got %d", len(clusters)) + } + + if clusters[0].ID != 18 { + t.Errorf("expected clusterID 1; got %d", clusters[0].ID) + } +} + +func TestGetCluster(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + pid := 1234 + + mux.HandleFunc("/api/v4/projects/1234/clusters/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + response := `{ + "id":18, + "name":"cluster-1", + "created_at":"2019-01-02T20:18:12.563Z", + "provider_type":"user", + "platform_type":"kubernetes", + "environment_scope":"*", + "cluster_type":"project_type", + "user": + { + "id":1, + "name":"Administrator", + "username":"root", + "state":"active", + "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..", + "web_url":"https://gitlab.example.com/root" + }, + "platform_kubernetes": + { + "api_url":"https://104.197.68.152", + "namespace":"cluster-1-namespace", + "authorization_type":"rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----" + }, + "project": + { + "id":26, + "description":"", + "name":"project-with-clusters-api", + "name_with_namespace":"Administrator / project-with-clusters-api", + "path":"project-with-clusters-api", + "path_with_namespace":"root/project-with-clusters-api", + "created_at":"2019-01-02T20:13:32.600Z", + "default_branch":null, + "tag_list":[], + "ssh_url_to_repo":"ssh://gitlab.example.com/root/project-with-clusters-api.git", + "http_url_to_repo":"https://gitlab.example.com/root/project-with-clusters-api.git", + "web_url":"https://gitlab.example.com/root/project-with-clusters-api", + "readme_url":null, + "avatar_url":null, + "star_count":0, + "forks_count":0, + "last_activity_at":"2019-01-02T20:13:32.600Z", + "namespace": + { + "id":1, + "name":"root", + "path":"root", + "kind":"user", + "full_path":"root", + "parent_id":null + } + } +}` + fmt.Fprint(w, response) + }) + + cluster, _, err := client.ProjectCluster.GetCluster(pid, 1) + + if err != nil { + t.Errorf("ProjectClusters.ListClusters returned error: %v", err) + } + + if cluster.ID != 18 { + t.Errorf("expected clusterID 18; got %d", cluster.ID) + } +} + +func TestAddCluster(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + pid := 1234 + + mux.HandleFunc("/api/v4/projects/1234/clusters/user", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + response := `{ + "id":24, + "name":"cluster-5", + "created_at":"2019-01-03T21:53:40.610Z", + "provider_type":"user", + "platform_type":"kubernetes", + "environment_scope":"*", + "cluster_type":"project_type", + "user": + { + "id":1, + "name":"Administrator", + "username":"root", + "state":"active", + "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..", + "web_url":"https://gitlab.example.com/root" + }, + "platform_kubernetes": + { + "api_url":"https://35.111.51.20", + "namespace":"cluster-5-namespace", + "authorization_type":"rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----" + }, + "project": + { + "id":26, + "description":"", + "name":"project-with-clusters-api", + "name_with_namespace":"Administrator / project-with-clusters-api", + "path":"project-with-clusters-api", + "path_with_namespace":"root/project-with-clusters-api", + "created_at":"2019-01-02T20:13:32.600Z", + "default_branch":null, + "tag_list":[], + "ssh_url_to_repo":"ssh:://gitlab.example.com/root/project-with-clusters-api.git", + "http_url_to_repo":"https://gitlab.example.com/root/project-with-clusters-api.git", + "web_url":"https://gitlab.example.com/root/project-with-clusters-api", + "readme_url":null, + "avatar_url":null, + "star_count":0, + "forks_count":0, + "last_activity_at":"2019-01-02T20:13:32.600Z", + "namespace": + { + "id":1, + "name":"root", + "path":"root", + "kind":"user", + "full_path":"root", + "parent_id":null + } + } +}` + fmt.Fprint(w, response) + }) + + cluster, _, err := client.ProjectCluster.AddCluster(pid, &AddClusterOptions{}) + + if err != nil { + t.Errorf("ProjectClusters.AddCluster returned error: %v", err) + } + + if cluster.ID != 24 { + t.Errorf("expected ClusterID 24; got %d", cluster.ID) + } +} + +func TestEditCluster(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + pid := 1234 + + mux.HandleFunc("/api/v4/projects/1234/clusters/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + response := `{ + "id":24, + "name":"new-cluster-name", + "created_at":"2019-01-03T21:53:40.610Z", + "provider_type":"user", + "platform_type":"kubernetes", + "environment_scope":"*", + "cluster_type":"project_type", + "user": + { + "id":1, + "name":"Administrator", + "username":"root", + "state":"active", + "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..", + "web_url":"https://gitlab.example.com/root" + }, + "platform_kubernetes": + { + "api_url":"https://new-api-url.com", + "namespace":"cluster-5-namespace", + "authorization_type":"rbac", + "ca_cert":null + }, + "project": + { + "id":26, + "description":"", + "name":"project-with-clusters-api", + "name_with_namespace":"Administrator / project-with-clusters-api", + "path":"project-with-clusters-api", + "path_with_namespace":"root/project-with-clusters-api", + "created_at":"2019-01-02T20:13:32.600Z", + "default_branch":null, + "tag_list":[], + "ssh_url_to_repo":"ssh:://gitlab.example.com/root/project-with-clusters-api.git", + "http_url_to_repo":"https://gitlab.example.com/root/project-with-clusters-api.git", + "web_url":"https://gitlab.example.com/root/project-with-clusters-api", + "readme_url":null, + "avatar_url":null, + "star_count":0, + "forks_count":0, + "last_activity_at":"2019-01-02T20:13:32.600Z", + "namespace": + { + "id":1, + "name":"root", + "path":"root", + "kind":"user", + "full_path":"root", + "parent_id":null + } + } +}` + fmt.Fprint(w, response) + }) + + cluster, _, err := client.ProjectCluster.EditCluster(pid, 1, &EditClusterOptions{}) + + if err != nil { + t.Errorf("ProjectClusters.EditCluster returned error: %v", err) + } + + if cluster.ID != 24 { + t.Errorf("expected ClusterID 24; got %d", cluster.ID) + } +} + +func TestDeleteCluster(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1234/clusters/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusAccepted) + }) + + resp, err := client.ProjectCluster.DeleteCluster(1234, 1) + if err != nil { + t.Errorf("ProjectCluster.DeleteCluster returned error: %v", err) + } + + want := http.StatusAccepted + got := resp.StatusCode + if got != want { + t.Errorf("ProjectCluster.DeleteCluster returned %d, want %d", got, want) + } +} diff --git a/api/project_members.go b/api/project_members.go new file mode 100644 index 0000000..94b618c --- /dev/null +++ b/api/project_members.go @@ -0,0 +1,208 @@ +// +// Copyright 2017, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// ProjectMembersService handles communication with the project members +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/members.html +type ProjectMembersService struct { + client *Client +} + +// ListProjectMembersOptions represents the available ListProjectMembers() and +// ListAllProjectMembers() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project +type ListProjectMembersOptions struct { + ListOptions + Query *string `url:"query,omitempty" json:"query,omitempty"` +} + +// ListProjectMembers gets a list of a project's team members viewable by the +// authenticated user. Returns only direct members and not inherited members +// through ancestors groups. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project +func (s *ProjectMembersService) ListProjectMembers(pid interface{}, opt *ListProjectMembersOptions, options ...OptionFunc) ([]*ProjectMember, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/members", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var pm []*ProjectMember + resp, err := s.client.Do(req, &pm) + if err != nil { + return nil, resp, err + } + + return pm, resp, err +} + +// ListAllProjectMembers gets a list of a project's team members viewable by the +// authenticated user. Returns a list including inherited members through +// ancestor groups. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#list-all-members-of-a-group-or-project-including-inherited-members +func (s *ProjectMembersService) ListAllProjectMembers(pid interface{}, opt *ListProjectMembersOptions, options ...OptionFunc) ([]*ProjectMember, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/members/all", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var pm []*ProjectMember + resp, err := s.client.Do(req, &pm) + if err != nil { + return nil, resp, err + } + + return pm, resp, err +} + +// GetProjectMember gets a project team member. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#get-a-member-of-a-group-or-project +func (s *ProjectMembersService) GetProjectMember(pid interface{}, user int, options ...OptionFunc) (*ProjectMember, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/members/%d", url.QueryEscape(project), user) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + pm := new(ProjectMember) + resp, err := s.client.Do(req, pm) + if err != nil { + return nil, resp, err + } + + return pm, resp, err +} + +// AddProjectMemberOptions represents the available AddProjectMember() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#add-a-member-to-a-group-or-project +type AddProjectMemberOptions struct { + UserID *int `url:"user_id,omitempty" json:"user_id,omitempty"` + AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"` +} + +// AddProjectMember adds a user to a project team. This is an idempotent +// method and can be called multiple times with the same parameters. Adding +// team membership to a user that is already a member does not affect the +// existing membership. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#add-a-member-to-a-group-or-project +func (s *ProjectMembersService) AddProjectMember(pid interface{}, opt *AddProjectMemberOptions, options ...OptionFunc) (*ProjectMember, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/members", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + pm := new(ProjectMember) + resp, err := s.client.Do(req, pm) + if err != nil { + return nil, resp, err + } + + return pm, resp, err +} + +// EditProjectMemberOptions represents the available EditProjectMember() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#edit-a-member-of-a-group-or-project +type EditProjectMemberOptions struct { + AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"` +} + +// EditProjectMember updates a project team member to a specified access level.. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#edit-a-member-of-a-group-or-project +func (s *ProjectMembersService) EditProjectMember(pid interface{}, user int, opt *EditProjectMemberOptions, options ...OptionFunc) (*ProjectMember, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/members/%d", url.QueryEscape(project), user) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + pm := new(ProjectMember) + resp, err := s.client.Do(req, pm) + if err != nil { + return nil, resp, err + } + + return pm, resp, err +} + +// DeleteProjectMember removes a user from a project team. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/members.html#remove-a-member-from-a-group-or-project +func (s *ProjectMembersService) DeleteProjectMember(pid interface{}, user int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/members/%d", url.QueryEscape(project), user) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/project_snippets.go b/api/project_snippets.go index e9f60e7..0042819 100644 --- a/api/project_snippets.go +++ b/api/project_snippets.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,61 +20,32 @@ import ( "bytes" "fmt" "net/url" - "time" ) // ProjectSnippetsService handles communication with the project snippets // related methods of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/project_snippets.html +// GitLab API docs: https://docs.gitlab.com/ce/api/project_snippets.html type ProjectSnippetsService struct { client *Client } -// Snippet represents a GitLab project snippet. +// ListProjectSnippetsOptions represents the available ListSnippets() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/project_snippets.html -type Snippet struct { - ID int `json:"id"` - Title string `json:"title"` - FileName string `json:"file_name"` - Author struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - State string `json:"state"` - CreatedAt time.Time `json:"created_at"` - } `json:"author"` - ExpiresAt *time.Time `json:"expires_at"` - UpdatedAt time.Time `json:"updated_at"` - CreatedAt time.Time `json:"created_at"` -} - -func (s Snippet) String() string { - return Stringify(s) -} - -// ListSnippetsOptions represents the available ListSnippets() options. -// -// GitLab API docs: http://doc.gitlab.com/ce/api/project_snippets.html#list-snippets -type ListSnippetsOptions struct { - ListOptions -} +// GitLab API docs: https://docs.gitlab.com/ce/api/project_snippets.html#list-snippets +type ListProjectSnippetsOptions ListOptions // ListSnippets gets a list of project snippets. // -// GitLab API docs: http://doc.gitlab.com/ce/api/project_snippets.html#list-snippets -func (s *ProjectSnippetsService) ListSnippets( - pid interface{}, - opt *ListSnippetsOptions) ([]*Snippet, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/project_snippets.html#list-snippets +func (s *ProjectSnippetsService) ListSnippets(pid interface{}, opt *ListProjectSnippetsOptions, options ...OptionFunc) ([]*Snippet, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/snippets", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -91,17 +62,15 @@ func (s *ProjectSnippetsService) ListSnippets( // GetSnippet gets a single project snippet // // GitLab API docs: -// http://doc.gitlab.com/ce/api/project_snippets.html#single-snippet -func (s *ProjectSnippetsService) GetSnippet( - pid interface{}, - snippet int) (*Snippet, *Response, error) { +// https://docs.gitlab.com/ce/api/project_snippets.html#single-snippet +func (s *ProjectSnippetsService) GetSnippet(pid interface{}, snippet int, options ...OptionFunc) (*Snippet, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/snippets/%d", url.QueryEscape(project), snippet) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -115,32 +84,31 @@ func (s *ProjectSnippetsService) GetSnippet( return ps, resp, err } -// CreateSnippetOptions represents the available CreateSnippet() options. +// CreateProjectSnippetOptions represents the available CreateSnippet() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/project_snippets.html#create-new-snippet -type CreateSnippetOptions struct { - Title string `url:"title,omitempty" json:"title,omitempty"` - FileName string `url:"file_name,omitempty" json:"file_name,omitempty"` - Code string `url:"code,omitempty" json:"code,omitempty"` - VisibilityLevel VisibilityLevel `url:"visibility_level,omitempty" json:"visibility_level,omitempty"` +// https://docs.gitlab.com/ce/api/project_snippets.html#create-new-snippet +type CreateProjectSnippetOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + FileName *string `url:"file_name,omitempty" json:"file_name,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + Code *string `url:"code,omitempty" json:"code,omitempty"` + Visibility *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"` } // CreateSnippet creates a new project snippet. The user must have permission // to create new snippets. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/project_snippets.html#create-new-snippet -func (s *ProjectSnippetsService) CreateSnippet( - pid interface{}, - opt *CreateSnippetOptions) (*Snippet, *Response, error) { +// https://docs.gitlab.com/ce/api/project_snippets.html#create-new-snippet +func (s *ProjectSnippetsService) CreateSnippet(pid interface{}, opt *CreateProjectSnippetOptions, options ...OptionFunc) (*Snippet, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/snippets", url.QueryEscape(project)) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -154,33 +122,31 @@ func (s *ProjectSnippetsService) CreateSnippet( return ps, resp, err } -// UpdateSnippetOptions represents the available UpdateSnippet() options. +// UpdateProjectSnippetOptions represents the available UpdateSnippet() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/project_snippets.html#update-snippet -type UpdateSnippetOptions struct { - Title string `url:"title,omitempty" json:"title,omitempty"` - FileName string `url:"file_name,omitempty" json:"file_name,omitempty"` - Code string `url:"code,omitempty" json:"code,omitempty"` - VisibilityLevel VisibilityLevel `url:"visibility_level,omitempty" json:"visibility_level,omitempty"` +// https://docs.gitlab.com/ce/api/project_snippets.html#update-snippet +type UpdateProjectSnippetOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + FileName *string `url:"file_name,omitempty" json:"file_name,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + Code *string `url:"code,omitempty" json:"code,omitempty"` + Visibility *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"` } // UpdateSnippet updates an existing project snippet. The user must have // permission to change an existing snippet. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/project_snippets.html#update-snippet -func (s *ProjectSnippetsService) UpdateSnippet( - pid interface{}, - snippet int, - opt *UpdateSnippetOptions) (*Snippet, *Response, error) { +// https://docs.gitlab.com/ce/api/project_snippets.html#update-snippet +func (s *ProjectSnippetsService) UpdateSnippet(pid interface{}, snippet int, opt *UpdateProjectSnippetOptions, options ...OptionFunc) (*Snippet, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/snippets/%d", url.QueryEscape(project), snippet) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -199,41 +165,34 @@ func (s *ProjectSnippetsService) UpdateSnippet( // code. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/project_snippets.html#delete-snippet -func (s *ProjectSnippetsService) DeleteSnippet(pid interface{}, snippet int) (*Response, error) { +// https://docs.gitlab.com/ce/api/project_snippets.html#delete-snippet +func (s *ProjectSnippetsService) DeleteSnippet(pid interface{}, snippet int, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } u := fmt.Sprintf("projects/%s/snippets/%d", url.QueryEscape(project), snippet) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) - if err != nil { - return resp, err - } - - return resp, err + return s.client.Do(req, nil) } // SnippetContent returns the raw project snippet as plain text. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/project_snippets.html#snippet-content -func (s *ProjectSnippetsService) SnippetContent( - pid interface{}, - snippet int) ([]byte, *Response, error) { +// https://docs.gitlab.com/ce/api/project_snippets.html#snippet-content +func (s *ProjectSnippetsService) SnippetContent(pid interface{}, snippet int, options ...OptionFunc) ([]byte, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/snippets/%d/raw", url.QueryEscape(project), snippet) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } diff --git a/api/project_variables.go b/api/project_variables.go new file mode 100644 index 0000000..920d7c8 --- /dev/null +++ b/api/project_variables.go @@ -0,0 +1,194 @@ +// +// Copyright 2018, Patrick Webster +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// ProjectVariablesService handles communication with the +// project variables related methods of the GitLab API. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_level_variables.html +type ProjectVariablesService struct { + client *Client +} + +// ProjectVariable represents a GitLab Project Variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_level_variables.html +type ProjectVariable struct { + Key string `json:"key"` + Value string `json:"value"` + Protected bool `json:"protected"` + EnvironmentScope string `json:"environment_scope"` +} + +func (v ProjectVariable) String() string { + return Stringify(v) +} + +// ListVariables gets a list of all variables in a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_level_variables.html#list-project-variables +func (s *ProjectVariablesService) ListVariables(pid interface{}, options ...OptionFunc) ([]*ProjectVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var vs []*ProjectVariable + resp, err := s.client.Do(req, &vs) + if err != nil { + return nil, resp, err + } + + return vs, resp, err +} + +// GetVariable gets a variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_level_variables.html#show-variable-details +func (s *ProjectVariablesService) GetVariable(pid interface{}, key string, options ...OptionFunc) (*ProjectVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables/%s", url.QueryEscape(project), url.QueryEscape(key)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + v := new(ProjectVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// CreateVariableOptions represents the available +// CreateVariable() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_level_variables.html#create-variable +type CreateVariableOptions struct { + Key *string `url:"key,omitempty" json:"key,omitempty"` + Value *string `url:"value,omitempty" json:"value,omitempty"` + Protected *bool `url:"protected,omitempty" json:"protected,omitempty"` + EnvironmentScope *string `url:"environment_scope,omitempty" json:"environment_scope,omitempty"` +} + +// CreateVariable creates a new project variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_level_variables.html#create-variable +func (s *ProjectVariablesService) CreateVariable(pid interface{}, opt *CreateVariableOptions, options ...OptionFunc) (*ProjectVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + v := new(ProjectVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// UpdateVariableOptions represents the available +// UpdateVariable() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_level_variables.html#update-variable +type UpdateVariableOptions struct { + Value *string `url:"value,omitempty" json:"value,omitempty"` + Protected *bool `url:"protected,omitempty" json:"protected,omitempty"` + EnvironmentScope *string `url:"environment_scope,omitempty" json:"environment_scope,omitempty"` +} + +// UpdateVariable updates a project's variable +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_level_variables.html#update-variable +func (s *ProjectVariablesService) UpdateVariable(pid interface{}, key string, opt *UpdateVariableOptions, options ...OptionFunc) (*ProjectVariable, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/variables/%s", + url.QueryEscape(project), + url.QueryEscape(key), + ) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + v := new(ProjectVariable) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} + +// RemoveVariable removes a project's variable. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_level_variables.html#remove-variable +func (s *ProjectVariablesService) RemoveVariable(pid interface{}, key string, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/variables/%s", + url.QueryEscape(project), + url.QueryEscape(key), + ) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/projects.go b/api/projects.go index affe0a5..7164d5a 100644 --- a/api/projects.go +++ b/api/projects.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,73 +17,167 @@ package gitlab import ( + "bytes" "fmt" + "io" + "io/ioutil" + "mime/multipart" "net/url" + "os" "time" ) // ProjectsService handles communication with the repositories related methods // of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html type ProjectsService struct { client *Client } // Project represents a GitLab project. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html type Project struct { - ID *int `json:"id"` - Description *string `json:"description"` - DefaultBranch *string `json:"default_branch"` - Public *bool `json:"public"` - VisibilityLevel *VisibilityLevel `json:"visibility_level"` - SSHURLToRepo *string `json:"ssh_url_to_repo"` - HTTPURLToRepo *string `json:"http_url_to_repo"` - WebURL *string `json:"web_url"` - TagList *[]string `json:"tag_list"` - Owner *User `json:"owner"` - Name *string `json:"name"` - NameWithNamespace *string `json:"name_with_namespace"` - Path *string `json:"path"` - PathWithNamespace *string `json:"path_with_namespace"` - IssuesEnabled *bool `json:"issues_enabled"` - MergeRequestsEnabled *bool `json:"merge_requests_enabled"` - WikiEnabled *bool `json:"wiki_enabled"` - SnippetsEnabled *bool `json:"snippets_enabled"` - CreatedAt *time.Time `json:"created_at,omitempty"` - LastActivityAt *time.Time `json:"last_activity_at,omitempty"` - CreatorID *int `json:"creator_id"` - Namespace *ProjectNamespace `json:"namespace"` - Archived *bool `json:"archived"` - AvatarURL *string `json:"avatar_url"` - Permissions *Permissions `json:"permissions"` + ID int `json:"id"` + Description string `json:"description"` + DefaultBranch string `json:"default_branch"` + Public bool `json:"public"` + Visibility VisibilityValue `json:"visibility"` + SSHURLToRepo string `json:"ssh_url_to_repo"` + HTTPURLToRepo string `json:"http_url_to_repo"` + WebURL string `json:"web_url"` + ReadmeURL string `json:"readme_url"` + TagList []string `json:"tag_list"` + Owner *User `json:"owner"` + Name string `json:"name"` + NameWithNamespace string `json:"name_with_namespace"` + Path string `json:"path"` + PathWithNamespace string `json:"path_with_namespace"` + IssuesEnabled bool `json:"issues_enabled"` + OpenIssuesCount int `json:"open_issues_count"` + MergeRequestsEnabled bool `json:"merge_requests_enabled"` + ApprovalsBeforeMerge int `json:"approvals_before_merge"` + JobsEnabled bool `json:"jobs_enabled"` + WikiEnabled bool `json:"wiki_enabled"` + SnippetsEnabled bool `json:"snippets_enabled"` + ContainerRegistryEnabled bool `json:"container_registry_enabled"` + CreatedAt *time.Time `json:"created_at,omitempty"` + LastActivityAt *time.Time `json:"last_activity_at,omitempty"` + CreatorID int `json:"creator_id"` + Namespace *ProjectNamespace `json:"namespace"` + ImportStatus string `json:"import_status"` + ImportError string `json:"import_error"` + Permissions *Permissions `json:"permissions"` + Archived bool `json:"archived"` + AvatarURL string `json:"avatar_url"` + SharedRunnersEnabled bool `json:"shared_runners_enabled"` + ForksCount int `json:"forks_count"` + StarCount int `json:"star_count"` + RunnersToken string `json:"runners_token"` + PublicBuilds bool `json:"public_builds"` + OnlyAllowMergeIfPipelineSucceeds bool `json:"only_allow_merge_if_pipeline_succeeds"` + OnlyAllowMergeIfAllDiscussionsAreResolved bool `json:"only_allow_merge_if_all_discussions_are_resolved"` + LFSEnabled bool `json:"lfs_enabled"` + RequestAccessEnabled bool `json:"request_access_enabled"` + MergeMethod MergeMethodValue `json:"merge_method"` + ForkedFromProject *ForkParent `json:"forked_from_project"` + Mirror bool `json:"mirror"` + MirrorUserID int `json:"mirror_user_id"` + MirrorTriggerBuilds bool `json:"mirror_trigger_builds"` + OnlyMirrorProtectedBranches bool `json:"only_mirror_protected_branches"` + MirrorOverwritesDivergedBranches bool `json:"mirror_overwrites_diverged_branches"` + SharedWithGroups []struct { + GroupID int `json:"group_id"` + GroupName string `json:"group_name"` + GroupAccessLevel int `json:"group_access_level"` + } `json:"shared_with_groups"` + Statistics *ProjectStatistics `json:"statistics"` + Links *Links `json:"_links,omitempty"` + CIConfigPath *string `json:"ci_config_path"` + CustomAttributes []*CustomAttribute `json:"custom_attributes"` } +// Repository represents a repository. +type Repository struct { + Name string `json:"name"` + Description string `json:"description"` + WebURL string `json:"web_url"` + AvatarURL string `json:"avatar_url"` + GitSSHURL string `json:"git_ssh_url"` + GitHTTPURL string `json:"git_http_url"` + Namespace string `json:"namespace"` + Visibility VisibilityValue `json:"visibility"` + PathWithNamespace string `json:"path_with_namespace"` + DefaultBranch string `json:"default_branch"` + Homepage string `json:"homepage"` + URL string `json:"url"` + SSHURL string `json:"ssh_url"` + HTTPURL string `json:"http_url"` +} + +// ProjectNamespace represents a project namespace. type ProjectNamespace struct { - CreatedAt *time.Time `json:"created_at"` - Description *string `json:"description"` - ID *int `json:"id"` - Name *string `json:"name"` - OwnerID *int `json:"owner_id"` - Path *string `json:"path"` - UpdatedAt *time.Time `json:"updated_at"` + ID int `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Kind string `json:"kind"` + FullPath string `json:"full_path"` +} + +// StorageStatistics represents a statistics record for a group or project. +type StorageStatistics struct { + StorageSize int64 `json:"storage_size"` + RepositorySize int64 `json:"repository_size"` + LfsObjectsSize int64 `json:"lfs_objects_size"` + JobArtifactsSize int64 `json:"job_artifacts_size"` +} + +// ProjectStatistics represents a statistics record for a project. +type ProjectStatistics struct { + StorageStatistics + CommitCount int `json:"commit_count"` } +// Permissions represents permissions. type Permissions struct { ProjectAccess *ProjectAccess `json:"project_access"` GroupAccess *GroupAccess `json:"group_access"` } +// ProjectAccess represents project access. type ProjectAccess struct { - AccessLevel AccessLevel `json:"access_level"` - NotificationLevel NotificationLevel `json:"notification_level"` + AccessLevel AccessLevelValue `json:"access_level"` + NotificationLevel NotificationLevelValue `json:"notification_level"` } +// GroupAccess represents group access. type GroupAccess struct { - AccessLevel AccessLevel `json:"access_level"` - NotificationLevel NotificationLevel `json:"notification_level"` + AccessLevel AccessLevelValue `json:"access_level"` + NotificationLevel NotificationLevelValue `json:"notification_level"` +} + +// ForkParent represents the parent project when this is a fork. +type ForkParent struct { + HTTPURLToRepo string `json:"http_url_to_repo"` + ID int `json:"id"` + Name string `json:"name"` + NameWithNamespace string `json:"name_with_namespace"` + Path string `json:"path"` + PathWithNamespace string `json:"path_with_namespace"` + WebURL string `json:"web_url"` +} + +// Links represents a project web links for self, issues, merge_requests, +// repo_branches, labels, events, members. +type Links struct { + Self string `json:"self"` + Issues string `json:"issues"` + MergeRequests string `json:"merge_requests"` + RepoBranches string `json:"repo_branches"` + Labels string `json:"labels"` + Events string `json:"events"` + Members string `json:"members"` } func (s Project) String() string { @@ -92,21 +186,30 @@ func (s Project) String() string { // ListProjectsOptions represents the available ListProjects() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html#list-projects +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#list-projects type ListProjectsOptions struct { ListOptions - Archived bool `url:"archived,omitempty" json:"archived,omitempty"` - OrderBy string `url:"order_by,omitempty" json:"order_by,omitempty"` - Sort string `url:"sort,omitempty" json:"sort,omitempty"` - Search string `url:"search,omitempty" json:"search,omitempty"` - CIEnabledFirst bool `url:"ci_enabled_first,omitempty" json:"ci_enabled_first,omitempty"` + Archived *bool `url:"archived,omitempty" json:"archived,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` + Search *string `url:"search,omitempty" json:"search,omitempty"` + Simple *bool `url:"simple,omitempty" json:"simple,omitempty"` + Owned *bool `url:"owned,omitempty" json:"owned,omitempty"` + Membership *bool `url:"membership,omitempty" json:"membership,omitempty"` + Starred *bool `url:"starred,omitempty" json:"starred,omitempty"` + Statistics *bool `url:"statistics,omitempty" json:"statistics,omitempty"` + Visibility *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"` + WithIssuesEnabled *bool `url:"with_issues_enabled,omitempty" json:"with_issues_enabled,omitempty"` + WithMergeRequestsEnabled *bool `url:"with_merge_requests_enabled,omitempty" json:"with_merge_requests_enabled,omitempty"` + MinAccessLevel *AccessLevelValue `url:"min_access_level,omitempty" json:"min_access_level,omitempty"` + WithCustomAttributes *bool `url:"with_custom_attributes,omitempty" json:"with_custom_attributes,omitempty"` } // ListProjects gets a list of projects accessible by the authenticated user. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html#list-projects -func (s *ProjectsService) ListProjects(opt *ListProjectsOptions) ([]*Project, *Response, error) { - req, err := s.client.NewRequest("GET", "projects", opt) +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#list-projects +func (s *ProjectsService) ListProjects(opt *ListProjectsOptions, options ...OptionFunc) ([]*Project, *Response, error) { + req, err := s.client.NewRequest("GET", "projects", opt, options) if err != nil { return nil, nil, err } @@ -120,14 +223,18 @@ func (s *ProjectsService) ListProjects(opt *ListProjectsOptions) ([]*Project, *R return p, resp, err } -// ListOwnedProjects gets a list of projects which are owned by the -// authenticated user. +// ListUserProjects gets a list of projects for the given user. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#list-owned-projects -func (s *ProjectsService) ListOwnedProjects( - opt *ListProjectsOptions) ([]*Project, *Response, error) { - req, err := s.client.NewRequest("GET", "projects/owned", opt) +// https://docs.gitlab.com/ce/api/projects.html#list-user-projects +func (s *ProjectsService) ListUserProjects(uid interface{}, opt *ListProjectsOptions, options ...OptionFunc) ([]*Project, *Response, error) { + user, err := parseID(uid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("users/%s/projects", user) + + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -141,17 +248,41 @@ func (s *ProjectsService) ListOwnedProjects( return p, resp, err } -// ListAllProjects gets a list of all GitLab projects (admin only). +// ProjectUser represents a GitLab project user. +type ProjectUser struct { + ID int `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` +} + +// ListProjectUserOptions represents the available ListProjectsUsers() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#get-project-users +type ListProjectUserOptions struct { + ListOptions + Search *string `url:"search,omitempty" json:"search,omitempty"` +} + +// ListProjectsUsers gets a list of users for the given project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#list-all-projects -func (s *ProjectsService) ListAllProjects(opt *ListProjectsOptions) ([]*Project, *Response, error) { - req, err := s.client.NewRequest("GET", "projects/all", opt) +// https://docs.gitlab.com/ce/api/projects.html#get-project-users +func (s *ProjectsService) ListProjectsUsers(pid interface{}, opt *ListProjectUserOptions, options ...OptionFunc) ([]*ProjectUser, *Response, error) { + project, err := parseID(pid) if err != nil { return nil, nil, err } + u := fmt.Sprintf("projects/%s/users", url.QueryEscape(project)) - var p []*Project + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var p []*ProjectUser resp, err := s.client.Do(req, &p) if err != nil { return nil, resp, err @@ -160,24 +291,27 @@ func (s *ProjectsService) ListAllProjects(opt *ListProjectsOptions) ([]*Project, return p, resp, err } -// GetProject gets a specific project, identified by project ID or -// NAMESPACE/PROJECT_NAME, which is owned by the authenticated user. +// ProjectLanguages is a map of strings because the response is arbitrary // -// GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#get-single-project -func (s *ProjectsService) GetProject(pid interface{}) (*Project, *Response, error) { +// Gitlab API docs: https://docs.gitlab.com/ce/api/projects.html#languages +type ProjectLanguages map[string]float32 + +// GetProjectLanguages gets a list of languages used by the project +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#languages +func (s *ProjectsService) GetProjectLanguages(pid interface{}, options ...OptionFunc) (*ProjectLanguages, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s", url.QueryEscape(project)) + u := fmt.Sprintf("projects/%s/languages", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } - p := new(Project) + p := new(ProjectLanguages) resp, err := s.client.Do(req, p) if err != nil { return nil, resp, err @@ -186,33 +320,34 @@ func (s *ProjectsService) GetProject(pid interface{}) (*Project, *Response, erro return p, resp, err } -// SearchProjectsOptions represents the available SearchProjects() options. +// GetProjectOptions represents the available GetProject() options. // -// GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#search-for-projects-by-name -type SearchProjectsOptions struct { - ListOptions - OrderBy string `url:"order_by,omitempty" json:"order_by,omitempty"` - Sort string `url:"sort,omitempty" json:"sort,omitempty"` +// GitLab API docs: https://docs.gitlab.com/ee/api/projects.html#get-single-project +type GetProjectOptions struct { + Statistics *bool `url:"statistics,omitempty" json:"statistics,omitempty"` + License *bool `url:"license,omitempty" json:"license,omitempty"` + WithCustomAttributes *bool `url:"with_custom_attributes,omitempty" json:"with_custom_attributes,omitempty"` } -// SearchProjects searches for projects by name which are accessible to the -// authenticated user. +// GetProject gets a specific project, identified by project ID or +// NAMESPACE/PROJECT_NAME, which is owned by the authenticated user. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#search-for-projects-by-name -func (s *ProjectsService) SearchProjects( - query string, - opt *SearchProjectsOptions) ([]*Project, *Response, error) { - u := fmt.Sprintf("projects/search/%s", query) +// https://docs.gitlab.com/ce/api/projects.html#get-single-project +func (s *ProjectsService) GetProject(pid interface{}, opt *GetProjectOptions, options ...OptionFunc) (*Project, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } - var p []*Project - resp, err := s.client.Do(req, &p) + p := new(Project) + resp, err := s.client.Do(req, p) if err != nil { return nil, resp, err } @@ -223,7 +358,7 @@ func (s *ProjectsService) SearchProjects( // ProjectEvent represents a GitLab project event. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#get-project-events +// https://docs.gitlab.com/ce/api/projects.html#get-project-events type ProjectEvent struct { Title interface{} `json:"title"` ProjectID int `json:"project_id"` @@ -233,28 +368,14 @@ type ProjectEvent struct { AuthorID int `json:"author_id"` AuthorUsername string `json:"author_username"` Data struct { - Before string `json:"before"` - After string `json:"after"` - Ref string `json:"ref"` - UserID int `json:"user_id"` - UserName string `json:"user_name"` - Repository struct { - Name string `json:"name"` - URL string `json:"url"` - Description string `json:"description"` - Homepage string `json:"homepage"` - } `json:"repository"` - Commits []struct { - ID string `json:"id"` - Message string `json:"message"` - Timestamp time.Time `json:"timestamp"` - URL string `json:"url"` - Author struct { - Name string `json:"name"` - Email string `json:"email"` - } `json:"author"` - } `json:"commits"` - TotalCommitsCount int `json:"total_commits_count"` + Before string `json:"before"` + After string `json:"after"` + Ref string `json:"ref"` + UserID int `json:"user_id"` + UserName string `json:"user_name"` + Repository *Repository `json:"repository"` + Commits []*Commit `json:"commits"` + TotalCommitsCount int `json:"total_commits_count"` } `json:"data"` TargetTitle interface{} `json:"target_title"` } @@ -266,26 +387,22 @@ func (s ProjectEvent) String() string { // GetProjectEventsOptions represents the available GetProjectEvents() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#get-project-events -type GetProjectEventsOptions struct { - ListOptions -} +// https://docs.gitlab.com/ce/api/projects.html#get-project-events +type GetProjectEventsOptions ListOptions // GetProjectEvents gets the events for the specified project. Sorted from // newest to latest. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#get-project-events -func (s *ProjectsService) GetProjectEvents( - pid interface{}, - opt *GetProjectEventsOptions) ([]*ProjectEvent, *Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#get-project-events +func (s *ProjectsService) GetProjectEvents(pid interface{}, opt *GetProjectEventsOptions, options ...OptionFunc) ([]*ProjectEvent, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/events", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -301,27 +418,40 @@ func (s *ProjectsService) GetProjectEvents( // CreateProjectOptions represents the available CreateProjects() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html#create-project +// GitLab API docs: https://docs.gitlab.com/ee/api/projects.html#create-project type CreateProjectOptions struct { - Name string `url:"name,omitempty" json:"name,omitempty"` - Path string `url:"path,omitempty" json:"path,omitempty"` - NamespaceID string `url:"namespace_id,omitempty" json:"namespace_id,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` - IssuesEnabled bool `url:"issues_enabled,omitempty" json:"issues_enabled,omitempty"` - MergeRequestsEnabled bool `url:"merge_requests_enabled,omitempty" json:"merge_requests_enabled,omitempty"` - WikiEnabled bool `url:"wiki_enabled,omitempty" json:"wiki_enabled,omitempty"` - SnippetsEnabled bool `url:"snippets_enabled,omitempty" json:"snippets_enabled,omitempty"` - Public bool `url:"public,omitempty" json:"public,omitempty"` - VisibilityLevel VisibilityLevel `url:"visibility_level,omitempty" json:"visibility_level,omitempty"` - ImportURL string `url:"import_url,omitempty" json:"import_url,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + Path *string `url:"path,omitempty" json:"path,omitempty"` + DefaultBranch *string `url:"default_branch,omitempty" json:"default_branch,omitempty"` + NamespaceID *int `url:"namespace_id,omitempty" json:"namespace_id,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + IssuesEnabled *bool `url:"issues_enabled,omitempty" json:"issues_enabled,omitempty"` + MergeRequestsEnabled *bool `url:"merge_requests_enabled,omitempty" json:"merge_requests_enabled,omitempty"` + JobsEnabled *bool `url:"jobs_enabled,omitempty" json:"jobs_enabled,omitempty"` + WikiEnabled *bool `url:"wiki_enabled,omitempty" json:"wiki_enabled,omitempty"` + SnippetsEnabled *bool `url:"snippets_enabled,omitempty" json:"snippets_enabled,omitempty"` + ResolveOutdatedDiffDiscussions *bool `url:"resolve_outdated_diff_discussions,omitempty" json:"resolve_outdated_diff_discussions,omitempty"` + ContainerRegistryEnabled *bool `url:"container_registry_enabled,omitempty" json:"container_registry_enabled,omitempty"` + SharedRunnersEnabled *bool `url:"shared_runners_enabled,omitempty" json:"shared_runners_enabled,omitempty"` + Visibility *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"` + ImportURL *string `url:"import_url,omitempty" json:"import_url,omitempty"` + PublicBuilds *bool `url:"public_builds,omitempty" json:"public_builds,omitempty"` + OnlyAllowMergeIfPipelineSucceeds *bool `url:"only_allow_merge_if_pipeline_succeeds,omitempty" json:"only_allow_merge_if_pipeline_succeeds,omitempty"` + OnlyAllowMergeIfAllDiscussionsAreResolved *bool `url:"only_allow_merge_if_all_discussions_are_resolved,omitempty" json:"only_allow_merge_if_all_discussions_are_resolved,omitempty"` + MergeMethod *MergeMethodValue `url:"merge_method,omitempty" json:"merge_method,omitempty"` + LFSEnabled *bool `url:"lfs_enabled,omitempty" json:"lfs_enabled,omitempty"` + RequestAccessEnabled *bool `url:"request_access_enabled,omitempty" json:"request_access_enabled,omitempty"` + TagList *[]string `url:"tag_list,omitempty" json:"tag_list,omitempty"` + PrintingMergeRequestLinkEnabled *bool `url:"printing_merge_request_link_enabled,omitempty" json:"printing_merge_request_link_enabled,omitempty"` + CIConfigPath *string `url:"ci_config_path,omitempty" json:"ci_config_path,omitempty"` + ApprovalsBeforeMerge *int `url:"approvals_before_merge" json:"approvals_before_merge"` } // CreateProject creates a new project owned by the authenticated user. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html#create-project -func (s *ProjectsService) CreateProject( - opt *CreateProjectOptions) (*Project, *Response, error) { - req, err := s.client.NewRequest("POST", "projects", opt) +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#create-project +func (s *ProjectsService) CreateProject(opt *CreateProjectOptions, options ...OptionFunc) (*Project, *Response, error) { + req, err := s.client.NewRequest("POST", "projects", opt, options) if err != nil { return nil, nil, err } @@ -339,31 +469,18 @@ func (s *ProjectsService) CreateProject( // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#create-project-for-user -type CreateProjectForUserOptions struct { - Name string `url:"name,omitempty" json:"name,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` - DefaultBranch string `url:"default_branch,omitempty" json:"default_branch,omitempty"` - IssuesEnabled bool `url:"issues_enabled,omitempty" json:"issues_enabled,omitempty"` - MergeRequestsEnabled bool `url:"merge_requests_enabled,omitempty" json:"merge_requests_enabled,omitempty"` - WikiEnabled bool `url:"wiki_enabled,omitempty" json:"wiki_enabled,omitempty"` - SnippetsEnabled bool `url:"snippets_enabled,omitempty" json:"snippets_enabled,omitempty"` - Public bool `url:"public,omitempty" json:"public,omitempty"` - VisibilityLevel VisibilityLevel `url:"visibility_level,omitempty" json:"visibility_level,omitempty"` - ImportURL string `url:"import_url,omitempty" json:"import_url,omitempty"` -} +// https://docs.gitlab.com/ce/api/projects.html#create-project-for-user +type CreateProjectForUserOptions CreateProjectOptions // CreateProjectForUser creates a new project owned by the specified user. // Available only for admins. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#create-project-for-user -func (s *ProjectsService) CreateProjectForUser( - user int, - opt *CreateProjectForUserOptions) (*Project, *Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#create-project-for-user +func (s *ProjectsService) CreateProjectForUser(user int, opt *CreateProjectForUserOptions, options ...OptionFunc) (*Project, *Response, error) { u := fmt.Sprintf("projects/user/%d", user) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -379,33 +496,20 @@ func (s *ProjectsService) CreateProjectForUser( // EditProjectOptions represents the available EditProject() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html#edit-project -type EditProjectOptions struct { - Name string `url:"name,omitempty" json:"name,omitempty"` - Path string `url:"path,omitempty" json:"path,omitempty"` - Description string `url:"description,omitempty" json:"description,omitempty"` - DefaultBranch string `url:"default_branch,omitempty" json:"default_branch,omitempty"` - IssuesEnabled bool `url:"issues_enabled,omitempty" json:"issues_enabled,omitempty"` - MergeRequestsEnabled bool `url:"merge_requests_enabled,omitempty" json:"merge_requests_enabled,omitempty"` - WikiEnabled bool `url:"wiki_enabled,omitempty" json:"wiki_enabled,omitempty"` - SnippetsEnabled bool `url:"snippets_enabled,omitempty" json:"snippets_enabled,omitempty"` - Public bool `url:"public,omitempty" json:"public,omitempty"` - VisibilityLevel VisibilityLevel `url:"visibility_level,omitempty" json:"visibility_level,omitempty"` -} +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#edit-project +type EditProjectOptions CreateProjectOptions // EditProject updates an existing project. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html#edit-project -func (s *ProjectsService) EditProject( - pid interface{}, - opt *EditProjectOptions) (*Project, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#edit-project +func (s *ProjectsService) EditProject(pid interface{}, opt *EditProjectOptions, options ...OptionFunc) (*Project, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s", url.QueryEscape(project)) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -422,15 +526,15 @@ func (s *ProjectsService) EditProject( // ForkProject forks a project into the user namespace of the authenticated // user. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html#fork-project -func (s *ProjectsService) ForkProject(pid interface{}) (*Project, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#fork-project +func (s *ProjectsService) ForkProject(pid interface{}, options ...OptionFunc) (*Project, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/fork/%s", url.QueryEscape(project)) + u := fmt.Sprintf("projects/%s/fork", url.QueryEscape(project)) - req, err := s.client.NewRequest("POST", u, nil) + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { return nil, nil, err } @@ -444,242 +548,224 @@ func (s *ProjectsService) ForkProject(pid interface{}) (*Project, *Response, err return p, resp, err } -// DeleteProject removes a project including all associated resources -// (issues, merge requests etc.) +// StarProject stars a given the project. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html#remove-project -func (s *ProjectsService) DeleteProject(pid interface{}) (*Response, error) { +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#star-a-project +func (s *ProjectsService) StarProject(pid interface{}, options ...OptionFunc) (*Project, *Response, error) { project, err := parseID(pid) if err != nil { - return nil, err + return nil, nil, err } - u := fmt.Sprintf("projects/%s", url.QueryEscape(project)) + u := fmt.Sprintf("projects/%s/star", url.QueryEscape(project)) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { - return nil, err + return nil, nil, err } - resp, err := s.client.Do(req, nil) + p := new(Project) + resp, err := s.client.Do(req, p) if err != nil { - return resp, err + return nil, resp, err } - return resp, err -} - -// ProjectMember represents a project member. -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#list-project-team-members -type ProjectMember struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - State string `json:"state"` - CreatedAt time.Time `json:"created_at"` - AccessLevel int `json:"access_level"` -} - -// ListProjectMembersOptions represents the available ListProjectMembers() -// options. -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#list-project-team-members -type ListProjectMembersOptions struct { - ListOptions - Query string `url:"query,omitempty" json:"query,omitempty"` + return p, resp, err } -// ListProjectMembers gets a list of a project's team members. +// UnstarProject unstars a given project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#list-project-team-members -func (s *ProjectsService) ListProjectMembers( - pid interface{}, - opt *ListProjectMembersOptions) ([]*ProjectMember, *Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#unstar-a-project +func (s *ProjectsService) UnstarProject(pid interface{}, options ...OptionFunc) (*Project, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/members", url.QueryEscape(project)) + u := fmt.Sprintf("projects/%s/unstar", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { return nil, nil, err } - var pm []*ProjectMember - resp, err := s.client.Do(req, &pm) + p := new(Project) + resp, err := s.client.Do(req, p) if err != nil { return nil, resp, err } - return pm, resp, err + return p, resp, err } -// GetProjectMember gets a project team member. +// ArchiveProject archives the project if the user is either admin or the +// project owner of this project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#get-project-team-member -func (s *ProjectsService) GetProjectMember( - pid interface{}, - user int) (*ProjectMember, *Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#archive-a-project +func (s *ProjectsService) ArchiveProject(pid interface{}, options ...OptionFunc) (*Project, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/members/%d", url.QueryEscape(project), user) + u := fmt.Sprintf("projects/%s/archive", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { return nil, nil, err } - pm := new(ProjectMember) - resp, err := s.client.Do(req, pm) + p := new(Project) + resp, err := s.client.Do(req, p) if err != nil { return nil, resp, err } - return pm, resp, err -} - -// AddProjectMemberOptions represents the available AddProjectMember() options. -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#add-project-team-member -type AddProjectMemberOptions struct { - UserID int `url:"user_id,omitempty" json:"user_id,omitempty"` - AccessLevel AccessLevel `url:"access_level,omitempty" json:"access_level,omitempty"` + return p, resp, err } -// AddProjectMember adds a user to a project team. This is an idempotent -// method and can be called multiple times with the same parameters. Adding -// team membership to a user that is already a member does not affect the -// existing membership. +// UnarchiveProject unarchives the project if the user is either admin or +// the project owner of this project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#add-project-team-member -func (s *ProjectsService) AddProjectMember( - pid interface{}, - opt *AddProjectMemberOptions) (*ProjectMember, *Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#unarchive-a-project +func (s *ProjectsService) UnarchiveProject(pid interface{}, options ...OptionFunc) (*Project, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/members", url.QueryEscape(project)) + u := fmt.Sprintf("projects/%s/unarchive", url.QueryEscape(project)) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { return nil, nil, err } - pm := new(ProjectMember) - resp, err := s.client.Do(req, pm) + p := new(Project) + resp, err := s.client.Do(req, p) if err != nil { return nil, resp, err } - return pm, resp, err -} - -// EditProjectMemberOptions represents the available EditProjectMember() options. -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#edit-project-team-member -type EditProjectMemberOptions struct { - AccessLevel AccessLevel `url:"access_level,omitempty" json:"access_level,omitempty"` + return p, resp, err } -// EditProjectMember updates a project team member to a specified access level.. +// DeleteProject removes a project including all associated resources +// (issues, merge requests etc.) // -// GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#edit-project-team-member -func (s *ProjectsService) EditProjectMember( - pid interface{}, - user int, - opt *EditProjectMemberOptions) (*ProjectMember, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#remove-project +func (s *ProjectsService) DeleteProject(pid interface{}, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { - return nil, nil, err + return nil, err } - u := fmt.Sprintf("projects/%s/members/%d", url.QueryEscape(project), user) + u := fmt.Sprintf("projects/%s", url.QueryEscape(project)) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { - return nil, nil, err + return nil, err } - pm := new(ProjectMember) - resp, err := s.client.Do(req, pm) - if err != nil { - return nil, resp, err - } + return s.client.Do(req, nil) +} - return pm, resp, err +// ShareWithGroupOptions represents options to share project with groups +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#share-project-with-group +type ShareWithGroupOptions struct { + GroupID *int `url:"group_id" json:"group_id"` + GroupAccess *AccessLevelValue `url:"group_access" json:"group_access"` + ExpiresAt *string `url:"expires_at" json:"expires_at"` } -// DeleteProjectMember removes a user from a project team. +// ShareProjectWithGroup allows to share a project with a group. // -// GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#remove-project-team-member -func (s *ProjectsService) DeleteProjectMember(pid interface{}, user int) (*Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#share-project-with-group +func (s *ProjectsService) ShareProjectWithGroup(pid interface{}, opt *ShareWithGroupOptions, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } - u := fmt.Sprintf("projects/%s/members/%d", url.QueryEscape(project), user) + u := fmt.Sprintf("projects/%s/share", url.QueryEscape(project)) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + return s.client.Do(req, nil) +} + +// DeleteSharedProjectFromGroup allows to unshare a project from a group. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#delete-a-shared-project-link-within-a-group +func (s *ProjectsService) DeleteSharedProjectFromGroup(pid interface{}, groupID int, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) if err != nil { - return resp, err + return nil, err + } + u := fmt.Sprintf("projects/%s/share/%d", url.QueryEscape(project), groupID) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err } - return resp, err + return s.client.Do(req, nil) +} + +// ProjectMember represents a project member. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#list-project-team-members +type ProjectMember struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + AccessLevel AccessLevelValue `json:"access_level"` } // ProjectHook represents a project hook. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#list-project-hooks +// https://docs.gitlab.com/ce/api/projects.html#list-project-hooks type ProjectHook 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"` - CreatedAt time.Time `json:"created_at"` + ID int `json:"id"` + URL string `json:"url"` + ProjectID int `json:"project_id"` + PushEvents bool `json:"push_events"` + IssuesEvents bool `json:"issues_events"` + ConfidentialIssuesEvents bool `json:"confidential_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"` } // ListProjectHooksOptions represents the available ListProjectHooks() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html#list-project-hooks -type ListProjectHooksOptions struct { - ListOptions -} +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#list-project-hooks +type ListProjectHooksOptions ListOptions // ListProjectHooks gets a list of project hooks. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#list-project-hooks -func (s *ProjectsService) ListProjectHooks( - pid interface{}, - opt *ListProjectHooksOptions) ([]*ProjectHook, *Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#list-project-hooks +func (s *ProjectsService) ListProjectHooks(pid interface{}, opt *ListProjectHooksOptions, options ...OptionFunc) ([]*ProjectHook, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/hooks", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -696,17 +782,15 @@ func (s *ProjectsService) ListProjectHooks( // GetProjectHook gets a specific hook for a project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#get-project-hook -func (s *ProjectsService) GetProjectHook( - pid interface{}, - hook int) (*ProjectHook, *Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#get-project-hook +func (s *ProjectsService) GetProjectHook(pid interface{}, hook int, options ...OptionFunc) (*ProjectHook, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/hooks/%d", url.QueryEscape(project), hook) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -723,29 +807,34 @@ func (s *ProjectsService) GetProjectHook( // AddProjectHookOptions represents the available AddProjectHook() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#add-project-hook +// https://docs.gitlab.com/ce/api/projects.html#add-project-hook type AddProjectHookOptions struct { - URL string `url:"url,omitempty" json:"url,omitempty"` - PushEvents bool `url:"push_events,omitempty" json:"push_events,omitempty"` - IssuesEvents bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` - MergeRequestsEvents bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` - TagPushEvents bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` + URL *string `url:"url,omitempty" json:"url,omitempty"` + PushEvents *bool `url:"push_events,omitempty" json:"push_events,omitempty"` + IssuesEvents *bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` + ConfidentialIssuesEvents *bool `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"` + MergeRequestsEvents *bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` + TagPushEvents *bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` + NoteEvents *bool `url:"note_events,omitempty" json:"note_events,omitempty"` + JobEvents *bool `url:"job_events,omitempty" json:"job_events,omitempty"` + PipelineEvents *bool `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"` + WikiPageEvents *bool `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"` + EnableSSLVerification *bool `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` + Token *string `url:"token,omitempty" json:"token,omitempty"` } // AddProjectHook adds a hook to a specified project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#add-project-hook -func (s *ProjectsService) AddProjectHook( - pid interface{}, - opt *AddProjectHookOptions) (*ProjectHook, *Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#add-project-hook +func (s *ProjectsService) AddProjectHook(pid interface{}, opt *AddProjectHookOptions, options ...OptionFunc) (*ProjectHook, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/hooks", url.QueryEscape(project)) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -762,30 +851,34 @@ func (s *ProjectsService) AddProjectHook( // EditProjectHookOptions represents the available EditProjectHook() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#edit-project-hook +// https://docs.gitlab.com/ce/api/projects.html#edit-project-hook type EditProjectHookOptions struct { - URL string `url:"url,omitempty" json:"url,omitempty"` - PushEvents bool `url:"push_events,omitempty" json:"push_events,omitempty"` - IssuesEvents bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` - MergeRequestsEvents bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` - TagPushEvents bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` + URL *string `url:"url,omitempty" json:"url,omitempty"` + PushEvents *bool `url:"push_events,omitempty" json:"push_events,omitempty"` + IssuesEvents *bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` + ConfidentialIssuesEvents *bool `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"` + MergeRequestsEvents *bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` + TagPushEvents *bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` + NoteEvents *bool `url:"note_events,omitempty" json:"note_events,omitempty"` + JobEvents *bool `url:"job_events,omitempty" json:"job_events,omitempty"` + PipelineEvents *bool `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"` + WikiPageEvents *bool `url:"wiki_page_events,omitempty" json:"wiki_page_events,omitempty"` + EnableSSLVerification *bool `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` + Token *string `url:"token,omitempty" json:"token,omitempty"` } // EditProjectHook edits a hook for a specified project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#edit-project-hook -func (s *ProjectsService) EditProjectHook( - pid interface{}, - hook int, - opt *EditProjectHookOptions) (*ProjectHook, *Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#edit-project-hook +func (s *ProjectsService) EditProjectHook(pid interface{}, hook int, opt *EditProjectHookOptions, options ...OptionFunc) (*ProjectHook, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/hooks/%d", url.QueryEscape(project), hook) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -803,50 +896,43 @@ func (s *ProjectsService) EditProjectHook( // method and can be called multiple times. Either the hook is available or not. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#delete-project-hook -func (s *ProjectsService) DeleteProjectHook(pid interface{}, hook int) (*Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#delete-project-hook +func (s *ProjectsService) DeleteProjectHook(pid interface{}, hook int, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } u := fmt.Sprintf("projects/%s/hooks/%d", url.QueryEscape(project), hook) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) - if err != nil { - return resp, err - } - - return resp, err + return s.client.Do(req, nil) } // ProjectForkRelation represents a project fork relationship. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#admin-fork-relation +// https://docs.gitlab.com/ce/api/projects.html#admin-fork-relation type ProjectForkRelation struct { - ID int `json:"id"` - ForkedToProjectID int `json:"forked_to_project_id"` - ForkedFromProjectID int `json:"forked_from_project_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID int `json:"id"` + ForkedToProjectID int `json:"forked_to_project_id"` + ForkedFromProjectID int `json:"forked_from_project_id"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` } // CreateProjectForkRelation creates a forked from/to relation between // existing projects. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#create-a-forked-fromto-relation-between-existing-projects. -func (s *ProjectsService) CreateProjectForkRelation( - pid int, - fork int) (*ProjectForkRelation, *Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#create-a-forked-fromto-relation-between-existing-projects. +func (s *ProjectsService) CreateProjectForkRelation(pid int, fork int, options ...OptionFunc) (*ProjectForkRelation, *Response, error) { u := fmt.Sprintf("projects/%d/fork/%d", pid, fork) - req, err := s.client.NewRequest("POST", u, nil) + req, err := s.client.NewRequest("POST", u, nil, options) if err != nil { return nil, nil, err } @@ -863,19 +949,351 @@ func (s *ProjectsService) CreateProjectForkRelation( // DeleteProjectForkRelation deletes an existing forked from relationship. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/projects.html#delete-an-existing-forked-from-relationship -func (s *ProjectsService) DeleteProjectForkRelation(pid int) (*Response, error) { +// https://docs.gitlab.com/ce/api/projects.html#delete-an-existing-forked-from-relationship +func (s *ProjectsService) DeleteProjectForkRelation(pid int, options ...OptionFunc) (*Response, error) { u := fmt.Sprintf("projects/%d/fork", pid) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ProjectFile represents an uploaded project file +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#upload-a-file +type ProjectFile struct { + Alt string `json:"alt"` + URL string `json:"url"` + Markdown string `json:"markdown"` +} + +// UploadFile upload a file from disk +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#upload-a-file +func (s *ProjectsService) UploadFile(pid interface{}, file string, options ...OptionFunc) (*ProjectFile, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/uploads", url.QueryEscape(project)) + + f, err := os.Open(file) + if err != nil { + return nil, nil, err + } + defer f.Close() + + b := &bytes.Buffer{} + w := multipart.NewWriter(b) + + fw, err := w.CreateFormFile("file", file) + if err != nil { + return nil, nil, err + } + + _, err = io.Copy(fw, f) + if err != nil { + return nil, nil, err + } + w.Close() + + req, err := s.client.NewRequest("", u, nil, options) + if err != nil { + return nil, nil, err + } + + req.Body = ioutil.NopCloser(b) + req.ContentLength = int64(b.Len()) + req.Header.Set("Content-Type", w.FormDataContentType()) + req.Method = "POST" + + uf := &ProjectFile{} + resp, err := s.client.Do(req, uf) + if err != nil { + return nil, resp, err + } + + return uf, resp, nil +} + +// ListProjectForks gets a list of project forks. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/projects.html#list-forks-of-a-project +func (s *ProjectsService) ListProjectForks(pid interface{}, opt *ListProjectsOptions, options ...OptionFunc) ([]*Project, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/forks", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var forks []*Project + resp, err := s.client.Do(req, &forks) + if err != nil { + return nil, resp, err + } + + return forks, resp, err +} + +// ProjectPushRules represents a project push rule. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#push-rules +type ProjectPushRules struct { + ID int `json:"id"` + ProjectID int `json:"project_id"` + CommitMessageRegex string `json:"commit_message_regex"` + BranchNameRegex string `json:"branch_name_regex"` + DenyDeleteTag bool `json:"deny_delete_tag"` + CreatedAt *time.Time `json:"created_at"` + MemberCheck bool `json:"member_check"` + PreventSecrets bool `json:"prevent_secrets"` + AuthorEmailRegex string `json:"author_email_regex"` + FileNameRegex string `json:"file_name_regex"` + MaxFileSize int `json:"max_file_size"` +} + +// GetProjectPushRules gets the push rules of a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#get-project-push-rules +func (s *ProjectsService) GetProjectPushRules(pid interface{}, options ...OptionFunc) (*ProjectPushRules, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/push_rule", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ppr := new(ProjectPushRules) + resp, err := s.client.Do(req, ppr) + if err != nil { + return nil, resp, err + } + + return ppr, resp, err +} + +// AddProjectPushRuleOptions represents the available AddProjectPushRule() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#add-project-push-rule +type AddProjectPushRuleOptions struct { + DenyDeleteTag *bool `url:"deny_delete_tag,omitempty" json:"deny_delete_tag,omitempty"` + MemberCheck *bool `url:"member_check,omitempty" json:"member_check,omitempty"` + PreventSecrets *bool `url:"prevent_secrets,omitempty" json:"prevent_secrets,omitempty"` + CommitMessageRegex *string `url:"commit_message_regex,omitempty" json:"commit_message_regex,omitempty"` + BranchNameRegex *string `url:"branch_name_regex,omitempty" json:"branch_name_regex,omitempty"` + AuthorEmailRegex *string `url:"author_email_regex,omitempty" json:"author_email_regex,omitempty"` + FileNameRegex *string `url:"file_name_regex,omitempty" json:"file_name_regex,omitempty"` + MaxFileSize *int `url:"max_file_size,omitempty" json:"max_file_size,omitempty"` +} + +// AddProjectPushRule adds a push rule to a specified project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#add-project-push-rule +func (s *ProjectsService) AddProjectPushRule(pid interface{}, opt *AddProjectPushRuleOptions, options ...OptionFunc) (*ProjectPushRules, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/push_rule", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + ppr := new(ProjectPushRules) + resp, err := s.client.Do(req, ppr) + if err != nil { + return nil, resp, err + } + + return ppr, resp, err +} + +// EditProjectPushRuleOptions represents the available EditProjectPushRule() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#edit-project-push-rule +type EditProjectPushRuleOptions struct { + AuthorEmailRegex *string `url:"author_email_regex,omitempty" json:"author_email_regex,omitempty"` + BranchNameRegex *string `url:"branch_name_regex,omitempty" json:"branch_name_regex,omitempty"` + CommitMessageRegex *string `url:"commit_message_regex,omitempty" json:"commit_message_regex,omitempty"` + FileNameRegex *string `url:"file_name_regex,omitempty" json:"file_name_regex,omitempty"` + DenyDeleteTag *bool `url:"deny_delete_tag,omitempty" json:"deny_delete_tag,omitempty"` + MemberCheck *bool `url:"member_check,omitempty" json:"member_check,omitempty"` + PreventSecrets *bool `url:"prevent_secrets,omitempty" json:"prevent_secrets,omitempty"` + MaxFileSize *int `url:"max_file_size,omitempty" json:"max_file_size,omitempty"` +} + +// EditProjectPushRule edits a push rule for a specified project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#edit-project-push-rule +func (s *ProjectsService) EditProjectPushRule(pid interface{}, opt *EditProjectPushRuleOptions, options ...OptionFunc) (*ProjectPushRules, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/push_rule", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ppr := new(ProjectPushRules) + resp, err := s.client.Do(req, ppr) + if err != nil { + return nil, resp, err + } + + return ppr, resp, err +} + +// DeleteProjectPushRule removes a push rule from a project. This is an +// idempotent method and can be called multiple times. Either the push rule is +// available or not. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#delete-project-push-rule +func (s *ProjectsService) DeleteProjectPushRule(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/push_rule", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + return s.client.Do(req, nil) +} + +// ProjectApprovals represents GitLab project level merge request approvals. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#project-level-mr-approvals +type ProjectApprovals struct { + Approvers []*MergeRequestApproverUser `json:"approvers"` + ApproverGroups []*MergeRequestApproverGroup `json:"approver_groups"` + ApprovalsBeforeMerge int `json:"approvals_before_merge"` + ResetApprovalsOnPush bool `json:"reset_approvals_on_push"` + DisableOverridingApproversPerMergeRequest bool `json:"disable_overriding_approvers_per_merge_request"` +} + +// GetApprovalConfiguration get the approval configuration for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-configuration +func (s *ProjectsService) GetApprovalConfiguration(pid interface{}, options ...OptionFunc) (*ProjectApprovals, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/approvals", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + pa := new(ProjectApprovals) + resp, err := s.client.Do(req, pa) + if err != nil { + return nil, resp, err + } + + return pa, resp, err +} + +// ChangeApprovalConfigurationOptions represents the available +// ApprovalConfiguration() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-configuration +type ChangeApprovalConfigurationOptions struct { + ApprovalsBeforeMerge *int `url:"approvals_before_merge,omitempty" json:"approvals_before_merge,omitempty"` + ResetApprovalsOnPush *bool `url:"reset_approvals_on_push,omitempty" json:"reset_approvals_on_push,omitempty"` + DisableOverridingApproversPerMergeRequest *bool `url:"disable_overriding_approvers_per_merge_request,omitempty" json:"disable_overriding_approvers_per_merge_request,omitempty"` +} + +// ChangeApprovalConfiguration updates the approval configuration for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-configuration +func (s *ProjectsService) ChangeApprovalConfiguration(pid interface{}, opt *ChangeApprovalConfigurationOptions, options ...OptionFunc) (*ProjectApprovals, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/approvals", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + pa := new(ProjectApprovals) + resp, err := s.client.Do(req, pa) if err != nil { - return resp, err + return nil, resp, err + } + + return pa, resp, err +} + +// ChangeAllowedApproversOptions represents the available ChangeAllowedApprovers() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-allowed-approvers +type ChangeAllowedApproversOptions struct { + ApproverIDs []*int `url:"approver_ids,omitempty" json:"approver_ids,omitempty"` + ApproverGroupIDs []*int `url:"approver_group_ids,omitempty" json:"approver_group_ids,omitempty"` +} + +// ChangeAllowedApprovers updates the list of approvers and approver groups. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-allowed-approvers +func (s *ProjectsService) ChangeAllowedApprovers(pid interface{}, opt *ChangeAllowedApproversOptions, options ...OptionFunc) (*ProjectApprovals, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/approvers", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + pa := new(ProjectApprovals) + resp, err := s.client.Do(req, pa) + if err != nil { + return nil, resp, err } - return resp, err + return pa, resp, err } diff --git a/api/projects_test.go b/api/projects_test.go index 0e97317..3a72c26 100644 --- a/api/projects_test.go +++ b/api/projects_test.go @@ -2,8 +2,11 @@ package gitlab import ( "fmt" + "io/ioutil" "net/http" + "os" "reflect" + "strings" "testing" ) @@ -11,107 +14,187 @@ func TestListProjects(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - testFormValues(t, r, values{ - "page": "2", - "per_page": "3", - "archived": "true", - "order_by": "name", - "sort": "asc", - "search": "query", - "ci_enabled_first": "true", - }) fmt.Fprint(w, `[{"id":1},{"id":2}]`) }) - opt := &ListProjectsOptions{ListOptions{2, 3}, true, "name", "asc", "query", true} - projects, _, err := client.Projects.ListProjects(opt) + opt := &ListProjectsOptions{ + ListOptions: ListOptions{2, 3}, + Archived: Bool(true), + OrderBy: String("name"), + Sort: String("asc"), + Search: String("query"), + Simple: Bool(true), + Visibility: Visibility(PublicVisibility), + } + projects, _, err := client.Projects.ListProjects(opt) if err != nil { t.Errorf("Projects.ListProjects returned error: %v", err) } - want := []*Project{{ID: Int(1)}, {ID: Int(2)}} + want := []*Project{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(want, projects) { t.Errorf("Projects.ListProjects returned %+v, want %+v", projects, want) } } +func TestListUserProjects(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/users/1/projects", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1},{"id":2}]`) + }) + + opt := &ListProjectsOptions{ + ListOptions: ListOptions{2, 3}, + Archived: Bool(true), + OrderBy: String("name"), + Sort: String("asc"), + Search: String("query"), + Simple: Bool(true), + Visibility: Visibility(PublicVisibility), + } + + projects, _, err := client.Projects.ListUserProjects(1, opt) + if err != nil { + t.Errorf("Projects.ListUserProjects returned error: %v", err) + } + + want := []*Project{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(want, projects) { + t.Errorf("Projects.ListUserProjects returned %+v, want %+v", projects, want) + } +} + +func TestListProjectsUsersByID(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/", func(w http.ResponseWriter, r *http.Request) { + testURL(t, r, "/api/v4/projects/1/users?page=2&per_page=3&search=query") + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1},{"id":2}]`) + }) + + opt := &ListProjectUserOptions{ + ListOptions: ListOptions{2, 3}, + Search: String("query"), + } + + projects, _, err := client.Projects.ListProjectsUsers(1, opt) + if err != nil { + t.Errorf("Projects.ListProjectsUsers returned error: %v", err) + } + + want := []*ProjectUser{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(want, projects) { + t.Errorf("Projects.ListProjectsUsers returned %+v, want %+v", projects, want) + } +} + +func TestListProjectsUsersByName(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/", func(w http.ResponseWriter, r *http.Request) { + testURL(t, r, "/api/v4/projects/namespace%2Fname/users?page=2&per_page=3&search=query") + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1},{"id":2}]`) + }) + + opt := &ListProjectUserOptions{ + ListOptions: ListOptions{2, 3}, + Search: String("query"), + } + + projects, _, err := client.Projects.ListProjectsUsers("namespace/name", opt) + if err != nil { + t.Errorf("Projects.ListProjectsUsers returned error: %v", err) + } + + want := []*ProjectUser{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(want, projects) { + t.Errorf("Projects.ListProjectsUsers returned %+v, want %+v", projects, want) + } +} + func TestListOwnedProjects(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects/owned", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - testFormValues(t, r, values{ - "page": "2", - "per_page": "3", - "archived": "true", - "order_by": "name", - "sort": "asc", - "search": "query", - "ci_enabled_first": "true", - }) fmt.Fprint(w, `[{"id":1},{"id":2}]`) }) - opt := &ListProjectsOptions{ListOptions{2, 3}, true, "name", "asc", "query", true} - projects, _, err := client.Projects.ListOwnedProjects(opt) + opt := &ListProjectsOptions{ + ListOptions: ListOptions{2, 3}, + Archived: Bool(true), + OrderBy: String("name"), + Sort: String("asc"), + Search: String("query"), + Simple: Bool(true), + Owned: Bool(true), + Visibility: Visibility(PublicVisibility), + } + projects, _, err := client.Projects.ListProjects(opt) if err != nil { t.Errorf("Projects.ListOwnedProjects returned error: %v", err) } - want := []*Project{{ID: Int(1)}, {ID: Int(2)}} + want := []*Project{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(want, projects) { t.Errorf("Projects.ListOwnedProjects returned %+v, want %+v", projects, want) } } -func TestListAllProjects(t *testing.T) { +func TestListStarredProjects(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects/all", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - testFormValues(t, r, values{ - "page": "2", - "per_page": "3", - "archived": "true", - "order_by": "name", - "sort": "asc", - "search": "query", - "ci_enabled_first": "true", - }) fmt.Fprint(w, `[{"id":1},{"id":2}]`) }) - opt := &ListProjectsOptions{ListOptions{2, 3}, true, "name", "asc", "query", true} - projects, _, err := client.Projects.ListAllProjects(opt) + opt := &ListProjectsOptions{ + ListOptions: ListOptions{2, 3}, + Archived: Bool(true), + OrderBy: String("name"), + Sort: String("asc"), + Search: String("query"), + Simple: Bool(true), + Starred: Bool(true), + Visibility: Visibility(PublicVisibility), + } + projects, _, err := client.Projects.ListProjects(opt) if err != nil { - t.Errorf("Projects.ListAllProjects returned error: %v", err) + t.Errorf("Projects.ListStarredProjects returned error: %v", err) } - want := []*Project{{ID: Int(1)}, {ID: Int(2)}} + want := []*Project{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(want, projects) { - t.Errorf("Projects.ListAllProjects returned %+v, want %+v", projects, want) + t.Errorf("Projects.ListStarredProjects returned %+v, want %+v", projects, want) } } -func TestGetProject_byID(t *testing.T) { +func TestGetProjectByID(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects/1", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects/1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{"id":1}`) }) - want := &Project{ID: Int(1)} - - project, _, err := client.Projects.GetProject(1) + want := &Project{ID: 1} + project, _, err := client.Projects.GetProject(1, nil) if err != nil { t.Fatalf("Projects.GetProject returns an error: %v", err) } @@ -121,19 +204,54 @@ func TestGetProject_byID(t *testing.T) { } } -func TestGetProject_byName(t *testing.T) { +func TestGetProjectByName(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects/", func(w http.ResponseWriter, r *http.Request) { - testUrl(t, r, "/projects/namespace%2Fname") + mux.HandleFunc("/api/v4/projects/", func(w http.ResponseWriter, r *http.Request) { + testURL(t, r, "/api/v4/projects/namespace%2Fname") testMethod(t, r, "GET") fmt.Fprint(w, `{"id":1}`) }) - want := &Project{ID: Int(1)} + want := &Project{ID: 1} - project, _, err := client.Projects.GetProject("namespace/name") + project, _, err := client.Projects.GetProject("namespace/name", nil) + if err != nil { + t.Fatalf("Projects.GetProject returns an error: %v", err) + } + if !reflect.DeepEqual(want, project) { + t.Errorf("Projects.GetProject returned %+v, want %+v", project, want) + } +} + +func TestGetProjectWithOptions(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "id":1, + "statistics": { + "commit_count": 37, + "storage_size": 1038090, + "repository_size": 1038090, + "lfs_objects_size": 0, + "job_artifacts_size": 0 + }}`) + }) + want := &Project{ID: 1, Statistics: &ProjectStatistics{ + CommitCount: 37, + StorageStatistics: StorageStatistics{ + StorageSize: 1038090, + RepositorySize: 1038090, + LfsObjectsSize: 0, + JobArtifactsSize: 0, + }, + }} + + project, _, err := client.Projects.GetProject(1, &GetProjectOptions{Statistics: Bool(true)}) if err != nil { t.Fatalf("Projects.GetProject returns an error: %v", err) } @@ -143,56 +261,264 @@ func TestGetProject_byName(t *testing.T) { } } -func TestSearchProjects(t *testing.T) { +func TestCreateProject(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects/search/query", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id":1}`) + }) + + opt := &CreateProjectOptions{ + Name: String("n"), + MergeMethod: MergeMethod(RebaseMerge), + } + + project, _, err := client.Projects.CreateProject(opt) + if err != nil { + t.Errorf("Projects.CreateProject returned error: %v", err) + } + + want := &Project{ID: 1} + if !reflect.DeepEqual(want, project) { + t.Errorf("Projects.CreateProject returned %+v, want %+v", project, want) + } +} + +func TestUploadFile(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + tf, _ := ioutil.TempFile(os.TempDir(), "test") + defer os.Remove(tf.Name()) + + mux.HandleFunc("/api/v4/projects/1/uploads", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPost) + if false == strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data;") { + t.Fatalf("Prokects.UploadFile request content-type %+v want multipart/form-data;", r.Header.Get("Content-Type")) + } + if r.ContentLength == -1 { + t.Fatalf("Prokects.UploadFile request content-length is -1") + } + fmt.Fprint(w, `{ + "alt": "dk", + "url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.md", + "markdown": "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)" + }`) + }) + + want := &ProjectFile{ + Alt: "dk", + URL: "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.md", + Markdown: "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)", + } + + file, _, err := client.Projects.UploadFile(1, tf.Name()) + + if err != nil { + t.Fatalf("Prokects.UploadFile returns an error: %v", err) + } + + if !reflect.DeepEqual(want, file) { + t.Errorf("Prokects.UploadFile returned %+v, want %+v", file, want) + } +} + +func TestListProjectForks(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/", func(w http.ResponseWriter, r *http.Request) { + testURL(t, r, "/api/v4/projects/namespace%2Fname/forks?archived=true&order_by=name&page=2&per_page=3&search=query&simple=true&sort=asc&visibility=public") testMethod(t, r, "GET") - testFormValues(t, r, values{ - "page": "2", - "per_page": "3", - "order_by": "name", - "sort": "asc", - }) fmt.Fprint(w, `[{"id":1},{"id":2}]`) }) - opt := &SearchProjectsOptions{ListOptions{2, 3}, "name", "asc"} - projects, _, err := client.Projects.SearchProjects("query", opt) + opt := &ListProjectsOptions{} + opt.ListOptions = ListOptions{2, 3} + opt.Archived = Bool(true) + opt.OrderBy = String("name") + opt.Sort = String("asc") + opt.Search = String("query") + opt.Simple = Bool(true) + opt.Visibility = Visibility(PublicVisibility) + projects, _, err := client.Projects.ListProjectForks("namespace/name", opt) if err != nil { - t.Errorf("Projects.SearchProjects returned error: %v", err) + t.Errorf("Projects.ListProjectForks returned error: %v", err) } - want := []*Project{{ID: Int(1)}, {ID: Int(2)}} + want := []*Project{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(want, projects) { - t.Errorf("Projects.SearchProjects returned %+v, want %+v", projects, want) + t.Errorf("Projects.ListProjects returned %+v, want %+v", projects, want) } } -func TestCreateProject(t *testing.T) { +func TestShareProjectWithGroup(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects/1/share", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "POST") - testJsonBody(t, r, values{ - "name": "n", - }) + }) - fmt.Fprint(w, `{"id":1}`) + opt := &ShareWithGroupOptions{ + GroupID: Int(1), + GroupAccess: AccessLevel(AccessLevelValue(50)), + } + + _, err := client.Projects.ShareProjectWithGroup(1, opt) + if err != nil { + t.Errorf("Projects.ShareProjectWithGroup returned error: %v", err) + } +} + +func TestDeleteSharedProjectFromGroup(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/share/2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") }) - opt := &CreateProjectOptions{Name: "n"} - project, _, err := client.Projects.CreateProject(opt) + _, err := client.Projects.DeleteSharedProjectFromGroup(1, 2) + if err != nil { + t.Errorf("Projects.DeleteSharedProjectFromGroup returned error: %v", err) + } +} + +func TestGetApprovalConfiguration(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + mux.HandleFunc("/api/v4/projects/1/approvals", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "approvers": [], + "approver_groups": [], + "approvals_before_merge": 3, + "reset_approvals_on_push": false, + "disable_overriding_approvers_per_merge_request": false + }`) + }) + + approvals, _, err := client.Projects.GetApprovalConfiguration(1) if err != nil { - t.Errorf("Projects.CreateProject returned error: %v", err) + t.Errorf("Projects.GetApprovalConfiguration returned error: %v", err) } - want := &Project{ID: Int(1)} - if !reflect.DeepEqual(want, project) { - t.Errorf("Projects.CreateProject returned %+v, want %+v", project, want) + want := &ProjectApprovals{ + Approvers: []*MergeRequestApproverUser{}, + ApproverGroups: []*MergeRequestApproverGroup{}, + ApprovalsBeforeMerge: 3, + ResetApprovalsOnPush: false, + DisableOverridingApproversPerMergeRequest: false, + } + + if !reflect.DeepEqual(want, approvals) { + t.Errorf("Projects.GetApprovalConfiguration returned %+v, want %+v", approvals, want) + } +} + +func TestChangeApprovalConfiguration(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/approvals", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, `{"approvals_before_merge":3}`) + fmt.Fprint(w, `{ + "approvers": [], + "approver_groups": [], + "approvals_before_merge": 3, + "reset_approvals_on_push": false, + "disable_overriding_approvers_per_merge_request": false + }`) + }) + + opt := &ChangeApprovalConfigurationOptions{ + ApprovalsBeforeMerge: Int(3), + } + + approvals, _, err := client.Projects.ChangeApprovalConfiguration(1, opt) + if err != nil { + t.Errorf("Projects.ChangeApprovalConfigurationOptions returned error: %v", err) + } + + want := &ProjectApprovals{ + Approvers: []*MergeRequestApproverUser{}, + ApproverGroups: []*MergeRequestApproverGroup{}, + ApprovalsBeforeMerge: 3, + ResetApprovalsOnPush: false, + DisableOverridingApproversPerMergeRequest: false, + } + + if !reflect.DeepEqual(want, approvals) { + t.Errorf("Projects.ChangeApprovalConfigurationOptions returned %+v, want %+v", approvals, want) + } +} + +func TestChangeAllowedApprovers(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/approvers", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, `{"approver_ids":[1],"approver_group_ids":[2]}`) + fmt.Fprint(w, `{ + "approvers": [{"user":{"id":1}}], + "approver_groups": [{"group":{"id":2}}] + }`) + }) + + opt := &ChangeAllowedApproversOptions{ + ApproverIDs: []*int{Int(1)}, + ApproverGroupIDs: []*int{Int(2)}, + } + + approvals, _, err := client.Projects.ChangeAllowedApprovers(1, opt) + if err != nil { + t.Errorf("Projects.ChangeApproversConfigurationOptions returned error: %v", err) + } + + want := &ProjectApprovals{ + Approvers: []*MergeRequestApproverUser{ + &MergeRequestApproverUser{ + User: struct { + ID int `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + }{ + ID: 1, + }, + }, + }, + ApproverGroups: []*MergeRequestApproverGroup{ + &MergeRequestApproverGroup{ + Group: struct { + ID int `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Description string `json:"description"` + Visibility string `json:"visibility"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + FullName string `json:"full_name"` + FullPath string `json:"full_path"` + LFSEnabled bool `json:"lfs_enabled"` + RequestAccessEnabled bool `json:"request_access_enabled"` + }{ + ID: 2, + }, + }, + }, + } + + if !reflect.DeepEqual(want, approvals) { + t.Errorf("Projects.ChangeAllowedApprovers returned %+v, want %+v", approvals, want) } } diff --git a/api/protected_branches.go b/api/protected_branches.go new file mode 100644 index 0000000..0a56241 --- /dev/null +++ b/api/protected_branches.go @@ -0,0 +1,165 @@ +// +// Copyright 2017, Sander van Harmelen, Michael Lihs +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// ProtectedBranchesService handles communication with the protected branch +// related methods of the GitLab API. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/protected_branches.html#protected-branches-api +type ProtectedBranchesService struct { + client *Client +} + +// BranchAccessDescription represents the access description for a protected +// branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/protected_branches.html#protected-branches-api +type BranchAccessDescription struct { + AccessLevel AccessLevelValue `json:"access_level"` + AccessLevelDescription string `json:"access_level_description"` +} + +// ProtectedBranch represents a protected branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/protected_branches.html#list-protected-branches +type ProtectedBranch struct { + Name string `json:"name"` + PushAccessLevels []*BranchAccessDescription `json:"push_access_levels"` + MergeAccessLevels []*BranchAccessDescription `json:"merge_access_levels"` +} + +// ListProtectedBranchesOptions represents the available ListProtectedBranches() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/protected_branches.html#list-protected-branches +type ListProtectedBranchesOptions ListOptions + +// ListProtectedBranches gets a list of protected branches from a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/protected_branches.html#list-protected-branches +func (s *ProtectedBranchesService) ListProtectedBranches(pid interface{}, opt *ListProtectedBranchesOptions, options ...OptionFunc) ([]*ProtectedBranch, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/protected_branches", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var p []*ProtectedBranch + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// GetProtectedBranch gets a single protected branch or wildcard protected branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/protected_branches.html#get-a-single-protected-branch-or-wildcard-protected-branch +func (s *ProtectedBranchesService) GetProtectedBranch(pid interface{}, branch string, options ...OptionFunc) (*ProtectedBranch, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/protected_branches/%s", url.QueryEscape(project), branch) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + p := new(ProtectedBranch) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// ProtectRepositoryBranchesOptions represents the available +// ProtectRepositoryBranches() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/protected_branches.html#protect-repository-branches +type ProtectRepositoryBranchesOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + PushAccessLevel *AccessLevelValue `url:"push_access_level,omitempty" json:"push_access_level,omitempty"` + MergeAccessLevel *AccessLevelValue `url:"merge_access_level,omitempty" json:"merge_access_level,omitempty"` +} + +// ProtectRepositoryBranches protects a single repository branch or several +// project repository branches using a wildcard protected branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/protected_branches.html#protect-repository-branches +func (s *ProtectedBranchesService) ProtectRepositoryBranches(pid interface{}, opt *ProtectRepositoryBranchesOptions, options ...OptionFunc) (*ProtectedBranch, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/protected_branches", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + p := new(ProtectedBranch) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// UnprotectRepositoryBranches unprotects the given protected branch or wildcard +// protected branch. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/protected_branches.html#unprotect-repository-branches +func (s *ProtectedBranchesService) UnprotectRepositoryBranches(pid interface{}, branch string, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/protected_branches/%s", url.QueryEscape(project), branch) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/protected_tags.go b/api/protected_tags.go new file mode 100644 index 0000000..d9b2ee8 --- /dev/null +++ b/api/protected_tags.go @@ -0,0 +1,146 @@ +package gitlab + +import ( + "fmt" + "net/url" +) + +// ProtectedTagsService handles communication with the protected tag methods +// of the GitLab API. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/protected_tags.html +type ProtectedTagsService struct { + client *Client +} + +// ProtectedTag represents a protected tag. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/protected_tags.html +type ProtectedTag struct { + Name string `json:"name"` + CreateAccessLevels []*TagAccessDescription `json:"create_access_levels"` +} + +// TagAccessDescription reperesents the access decription for a protected tag. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/protected_tags.html +type TagAccessDescription struct { + AccessLevel AccessLevelValue `json:"access_level"` + AccessLevelDescription string `json:"access_level_description"` +} + +// ListProtectedTagsOptions represents the available ListProtectedTags() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/protected_tags.html#list-protected-tags +type ListProtectedTagsOptions ListOptions + +// ListProtectedTags returns a list of protected tags from a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/protected_tags.html#list-protected-tags +func (s *ProtectedTagsService) ListProtectedTags(pid interface{}, opt *ListProtectedTagsOptions, options ...OptionFunc) ([]*ProtectedTag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/protected_tags", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var pts []*ProtectedTag + resp, err := s.client.Do(req, &pts) + if err != nil { + return nil, resp, err + } + + return pts, resp, err +} + +// GetProtectedTag returns a single protected tag or wildcard protected tag. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/protected_tags.html#get-a-single-protected-tag-or-wildcard-protected-tag +func (s *ProtectedTagsService) GetProtectedTag(pid interface{}, tag string, options ...OptionFunc) (*ProtectedTag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/protected_tags/%s", url.QueryEscape(project), tag) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + pt := new(ProtectedTag) + resp, err := s.client.Do(req, pt) + if err != nil { + return nil, resp, err + } + + return pt, resp, err +} + +// ProtectRepositoryTagsOptions represents the available ProtectRepositoryTags() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/protected_tags.html#protect-repository-tags +type ProtectRepositoryTagsOptions struct { + Name *string `url:"name" json:"name"` + CreateAccessLevel *AccessLevelValue `url:"create_access_level,omitempty" json:"create_access_level,omitempty"` +} + +// ProtectRepositoryTags protects a single repository tag or several project +// repository tags using a wildcard protected tag. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/protected_tags.html#protect-repository-tags +func (s *ProtectedTagsService) ProtectRepositoryTags(pid interface{}, opt *ProtectRepositoryTagsOptions, options ...OptionFunc) (*ProtectedTag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/protected_tags", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + pt := new(ProtectedTag) + resp, err := s.client.Do(req, pt) + if err != nil { + return nil, resp, err + } + + return pt, resp, err +} + +// UnprotectRepositoryTags unprotects the given protected tag or wildcard +// protected tag. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/protected_tags.html#unprotect-repository-tags +func (s *ProtectedTagsService) UnprotectRepositoryTags(pid interface{}, tag string, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/protected_tags/%s", url.QueryEscape(project), tag) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/protected_tags_test.go b/api/protected_tags_test.go new file mode 100644 index 0000000..19cabad --- /dev/null +++ b/api/protected_tags_test.go @@ -0,0 +1,179 @@ +package gitlab + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestListProtectedTags(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/protected_tags", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"name":"1.0.0", "create_access_levels": [{"access_level": 40, "access_level_description": "Maintainers"}]},{"name":"*-release", "create_access_levels": [{"access_level": 30, "access_level_description": "Developers + Maintainers"}]}]`) + }) + + expected := []*ProtectedTag{ + { + Name: "1.0.0", + CreateAccessLevels: []*TagAccessDescription{ + { + AccessLevel: 40, + AccessLevelDescription: "Maintainers", + }, + }, + }, + { + Name: "*-release", + CreateAccessLevels: []*TagAccessDescription{ + { + AccessLevel: 30, + AccessLevelDescription: "Developers + Maintainers", + }, + }, + }, + } + + opt := &ListProtectedTagsOptions{} + tags, _, err := client.ProtectedTags.ListProtectedTags(1, opt) + assert.NoError(t, err, "failed to get response") + assert.Equal(t, expected, tags) +} + +func TestListProtectedTags_WithServerError(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/protected_tags", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + w.WriteHeader(http.StatusInternalServerError) + }) + + opt := &ListProtectedTagsOptions{} + tags, resp, err := client.ProtectedTags.ListProtectedTags(1, opt) + + assert.Error(t, err) + assert.Nil(t, tags) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) +} + +func TestGetProtectedTag(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + tagName := "my-awesome-tag" + + mux.HandleFunc(fmt.Sprintf("/api/v4/projects/1/protected_tags/%s", tagName), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"name":"my-awesome-tag", "create_access_levels": [{"access_level": 30, "access_level_description": "Developers + Maintainers"}]}`) + }) + + expected := &ProtectedTag{ + Name: tagName, + CreateAccessLevels: []*TagAccessDescription{ + { + AccessLevel: 30, + AccessLevelDescription: "Developers + Maintainers", + }, + }, + } + + tag, _, err := client.ProtectedTags.GetProtectedTag(1, tagName) + + assert.NoError(t, err, "failed to get response") + assert.Equal(t, expected, tag) +} + +func TestGetProtectedTag_WithServerError(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + tagName := "my-awesome-tag" + + mux.HandleFunc(fmt.Sprintf("/api/v4/projects/1/protected_tags/%s", tagName), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + w.WriteHeader(http.StatusInternalServerError) + }) + + tag, resp, err := client.ProtectedTags.GetProtectedTag(1, tagName) + + assert.Error(t, err) + assert.Nil(t, tag) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) +} + +func TestProtectRepositoryTags(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/protected_tags", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"name":"my-awesome-tag", "create_access_levels": [{"access_level": 30, "access_level_description": "Developers + Maintainers"}]}`) + }) + + expected := &ProtectedTag{ + Name: "my-awesome-tag", + CreateAccessLevels: []*TagAccessDescription{ + { + AccessLevel: 30, + AccessLevelDescription: "Developers + Maintainers", + }, + }, + } + + opt := &ProtectRepositoryTagsOptions{Name: String("my-awesome-tag"), CreateAccessLevel: AccessLevel(30)} + tag, _, err := client.ProtectedTags.ProtectRepositoryTags(1, opt) + + assert.NoError(t, err, "failed to get response") + assert.Equal(t, expected, tag) +} + +func TestProtectRepositoryTags_WithServerError(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/protected_tags", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, `{"message":"some error"}`) + }) + + opt := &ProtectRepositoryTagsOptions{Name: String("my-awesome-tag"), CreateAccessLevel: AccessLevel(30)} + tag, resp, err := client.ProtectedTags.ProtectRepositoryTags(1, opt) + + assert.Nil(t, tag) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + assert.Error(t, err) +} + +func TestUnprotectRepositoryTags(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/protected_tags/my-awesome-tag", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + resp, err := client.ProtectedTags.UnprotectRepositoryTags(1, "my-awesome-tag") + assert.NoError(t, err, "failed to get response") + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestUnprotectRepositoryTags_WithServerError(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/protected_tags/my-awesome-tag", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, `{"message": "some error"}`) + }) + + resp, err := client.ProtectedTags.UnprotectRepositoryTags(1, "my-awesome-tag") + assert.Error(t, err) + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) +} diff --git a/api/repositories.go b/api/repositories.go index 22de6a2..451f4c7 100644 --- a/api/repositories.go +++ b/api/repositories.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,96 +25,19 @@ import ( // RepositoriesService handles communication with the repositories related // methods of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/repositories.html +// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html type RepositoriesService struct { client *Client } -// Tag represents a GitLab repository tag. -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#list-project-repository-tags -type Tag struct { - Commit *Commit `json:"commit"` - Name string `json:"name"` - Message string `json:"message"` -} - -func (r Tag) String() string { - return Stringify(r) -} - -// ListTags gets a list of repository tags from a project, sorted by name in -// reverse alphabetical order. -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#list-project-repository-tags -func (s *RepositoriesService) ListTags(pid interface{}) ([]*Tag, *Response, error) { - project, err := parseID(pid) - if err != nil { - return nil, nil, err - } - u := fmt.Sprintf("projects/%s/repository/tags", url.QueryEscape(project)) - - req, err := s.client.NewRequest("GET", u, nil) - if err != nil { - return nil, nil, err - } - - var t []*Tag - resp, err := s.client.Do(req, &t) - if err != nil { - return nil, resp, err - } - - return t, resp, err -} - -// CreateTagOptions represents the available CreateTag() options. -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#create-a-new-tag -type CreateTagOptions struct { - TagName string `url:"tag_name,omitempty" json:"tag_name,omitempty"` - Ref string `url:"ref,omitempty" json:"ref,omitempty"` - Message string `url:"message,omitempty" json:"message,omitempty"` -} - -// CreateTag creates a new tag in the repository that points to the supplied ref. -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#create-a-new-tag -func (s *RepositoriesService) CreateTag( - pid interface{}, - opt *CreateTagOptions) (*Tag, *Response, error) { - project, err := parseID(pid) - if err != nil { - return nil, nil, err - } - u := fmt.Sprintf("projects/%s/repository/tags", url.QueryEscape(project)) - - req, err := s.client.NewRequest("POST", u, opt) - if err != nil { - return nil, nil, err - } - - t := new(Tag) - resp, err := s.client.Do(req, t) - if err != nil { - return nil, resp, err - } - - return t, resp, err -} - // TreeNode represents a GitLab repository file or directory. // -// GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#list-repository-tree +// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html type TreeNode struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` + Path string `json:"path"` Mode string `json:"mode"` } @@ -125,26 +48,26 @@ func (t TreeNode) String() string { // ListTreeOptions represents the available ListTree() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#list-repository-tree +// https://docs.gitlab.com/ce/api/repositories.html#list-repository-tree type ListTreeOptions struct { - Path string `url:"path,omitempty" json:"path,omitempty"` - RefName string `url:"ref_name,omitempty" json:"ref_name,omitempty"` + ListOptions + Path *string `url:"path,omitempty" json:"path,omitempty"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` + Recursive *bool `url:"recursive,omitempty" json:"recursive,omitempty"` } // ListTree gets a list of repository files and directories in a project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#list-repository-tree -func (s *RepositoriesService) ListTree( - pid interface{}, - opt *ListTreeOptions) ([]*TreeNode, *Response, error) { +// https://docs.gitlab.com/ce/api/repositories.html#list-repository-tree +func (s *RepositoriesService) ListTree(pid interface{}, opt *ListTreeOptions, options ...OptionFunc) ([]*TreeNode, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/tree", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -158,29 +81,19 @@ func (s *RepositoriesService) ListTree( return t, resp, err } -// RawFileContentOptions represents the available RawFileContent() options. +// Blob gets information about blob in repository like size and content. Note +// that blob content is Base64 encoded. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#raw-file-content -type RawFileContentOptions struct { - FilePath string `url:"filepath,omitempty" json:"filepath,omitempty"` -} - -// RawFileContent gets the raw file contents for a file by commit SHA and path -// -// GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#raw-file-content -func (s *RepositoriesService) RawFileContent( - pid interface{}, - sha string, - opt *RawFileContentOptions) ([]byte, *Response, error) { +// https://docs.gitlab.com/ce/api/repositories.html#get-a-blob-from-repository +func (s *RepositoriesService) Blob(pid interface{}, sha string, options ...OptionFunc) ([]byte, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/blobs/%s", url.QueryEscape(project), sha) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -197,17 +110,15 @@ func (s *RepositoriesService) RawFileContent( // RawBlobContent gets the raw file contents for a blob by blob SHA. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#raw-blob-content -func (s *RepositoriesService) RawBlobContent( - pid interface{}, - sha string) ([]byte, *Response, error) { +// https://docs.gitlab.com/ce/api/repositories.html#raw-blob-content +func (s *RepositoriesService) RawBlobContent(pid interface{}, sha string, options ...OptionFunc) ([]byte, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/repository/raw_blobs/%s", url.QueryEscape(project), sha) + u := fmt.Sprintf("projects/%s/repository/blobs/%s/raw", url.QueryEscape(project), sha) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -224,25 +135,23 @@ func (s *RepositoriesService) RawBlobContent( // ArchiveOptions represents the available Archive() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#get-file-archive +// https://docs.gitlab.com/ce/api/repositories.html#get-file-archive type ArchiveOptions struct { - SHA string `url:"sha,omitempty" json:"sha,omitempty"` + SHA *string `url:"sha,omitempty" json:"sha,omitempty"` } // Archive gets an archive of the repository. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#get-file-archive -func (s *RepositoriesService) Archive( - pid interface{}, - opt *ArchiveOptions) ([]byte, *Response, error) { +// https://docs.gitlab.com/ce/api/repositories.html#get-file-archive +func (s *RepositoriesService) Archive(pid interface{}, opt *ArchiveOptions, options ...OptionFunc) ([]byte, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/archive", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -259,7 +168,7 @@ func (s *RepositoriesService) Archive( // Compare represents the result of a comparison of branches, tags or commits. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits +// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits type Compare struct { Commit *Commit `json:"commit"` Commits []*Commit `json:"commits"` @@ -275,26 +184,25 @@ func (c Compare) String() string { // CompareOptions represents the available Compare() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits +// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits type CompareOptions struct { - From string `url:"from,omitempty" json:"from,omitempty"` - To string `url:"to,omitempty" json:"to,omitempty"` + From *string `url:"from,omitempty" json:"from,omitempty"` + To *string `url:"to,omitempty" json:"to,omitempty"` + Straight *bool `url:"straight,omitempty" json:"straight,omitempty"` } // Compare compares branches, tags or commits. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits -func (s *RepositoriesService) Compare( - pid interface{}, - opt *CompareOptions) (*Compare, *Response, error) { +// https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits +func (s *RepositoriesService) Compare(pid interface{}, opt *CompareOptions, options ...OptionFunc) (*Compare, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/compare", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -310,7 +218,7 @@ func (s *RepositoriesService) Compare( // Contributor represents a GitLap contributor. // -// GitLab API docs: http://doc.gitlab.com/ce/api/repositories.html#contributer +// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributors type Contributor struct { Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` @@ -323,17 +231,22 @@ func (c Contributor) String() string { return Stringify(c) } +// ListContributorsOptions represents the available ListContributors() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributors +type ListContributorsOptions ListOptions + // Contributors gets the repository contributors list. // -// GitLab API docs: http://doc.gitlab.com/ce/api/repositories.html#contributer -func (s *RepositoriesService) Contributors(pid interface{}) ([]*Contributor, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/repositories.html#contributors +func (s *RepositoriesService) Contributors(pid interface{}, opt *ListContributorsOptions, options ...OptionFunc) ([]*Contributor, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } u := fmt.Sprintf("projects/%s/repository/contributors", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -346,3 +259,37 @@ func (s *RepositoriesService) Contributors(pid interface{}) ([]*Contributor, *Re return c, resp, err } + +// MergeBaseOptions represents the available MergeBase() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#merge-base +type MergeBaseOptions struct { + Ref []string `url:"refs[],omitempty" json:"refs,omitempty"` +} + +// MergeBase gets the common ancestor for 2 refs (commit SHAs, branch +// names or tags). +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repositories.html#merge-base +func (s *RepositoriesService) MergeBase(pid interface{}, opt *MergeBaseOptions, options ...OptionFunc) (*Commit, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/merge_base", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + c := new(Commit) + resp, err := s.client.Do(req, c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} diff --git a/api/repository_files.go b/api/repository_files.go index 10e64a3..5e8ce41 100644 --- a/api/repository_files.go +++ b/api/repository_files.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,21 +17,23 @@ package gitlab import ( + "bytes" "fmt" "net/url" + "strconv" ) // RepositoryFilesService handles communication with the repository files // related methods of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/repository_files.html +// GitLab API docs: https://docs.gitlab.com/ce/api/repository_files.html type RepositoryFilesService struct { client *Client } // File represents a GitLab repository file. // -// GitLab API docs: http://doc.gitlab.com/ce/api/repository_files.html +// GitLab API docs: https://docs.gitlab.com/ce/api/repository_files.html type File struct { FileName string `json:"file_name"` FilePath string `json:"file_path"` @@ -50,27 +52,28 @@ func (r File) String() string { // GetFileOptions represents the available GetFile() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repository_files.html#get-file-from-respository +// https://docs.gitlab.com/ce/api/repository_files.html#get-file-from-repository type GetFileOptions struct { - FilePath string `url:"file_path,omitempty" json:"file_path,omitempty"` - Ref string `url:"ref,omitempty" json:"ref,omitempty"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` } // GetFile allows you to receive information about a file in repository like // name, size, content. Note that file content is Base64 encoded. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repository_files.html#get-file-from-respository -func (s *RepositoryFilesService) GetFile( - pid interface{}, - opt *GetFileOptions) (*File, *Response, error) { +// https://docs.gitlab.com/ce/api/repository_files.html#get-file-from-repository +func (s *RepositoryFilesService) GetFile(pid interface{}, fileName string, opt *GetFileOptions, options ...OptionFunc) (*File, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/repository/files", url.QueryEscape(project)) + u := fmt.Sprintf( + "projects/%s/repository/files/%s", + url.QueryEscape(project), + url.PathEscape(fileName), + ) - req, err := s.client.NewRequest("GET", u, opt) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -84,12 +87,102 @@ func (s *RepositoryFilesService) GetFile( return f, resp, err } +// GetFileMetaDataOptions represents the available GetFileMetaData() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#get-file-from-repository +type GetFileMetaDataOptions struct { + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` +} + +// GetFileMetaData allows you to receive meta information about a file in +// repository like name, size. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#get-file-from-repository +func (s *RepositoryFilesService) GetFileMetaData(pid interface{}, fileName string, opt *GetFileMetaDataOptions, options ...OptionFunc) (*File, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf( + "projects/%s/repository/files/%s", + url.QueryEscape(project), + url.PathEscape(fileName), + ) + + req, err := s.client.NewRequest("HEAD", u, opt, options) + if err != nil { + return nil, nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return nil, resp, err + } + + f := &File{ + BlobID: resp.Header.Get("X-Gitlab-Blob-Id"), + CommitID: resp.Header.Get("X-Gitlab-Last-Commit-Id"), + Encoding: resp.Header.Get("X-Gitlab-Encoding"), + FileName: resp.Header.Get("X-Gitlab-File-Name"), + FilePath: resp.Header.Get("X-Gitlab-File-Path"), + Ref: resp.Header.Get("X-Gitlab-Ref"), + } + + if sizeString := resp.Header.Get("X-Gitlab-Size"); sizeString != "" { + f.Size, err = strconv.Atoi(sizeString) + if err != nil { + return nil, resp, err + } + } + + return f, resp, err +} + +// GetRawFileOptions represents the available GetRawFile() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#get-raw-file-from-repository +type GetRawFileOptions struct { + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` +} + +// GetRawFile allows you to receive the raw file in repository. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/repository_files.html#get-raw-file-from-repository +func (s *RepositoryFilesService) GetRawFile(pid interface{}, fileName string, opt *GetRawFileOptions, options ...OptionFunc) ([]byte, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf( + "projects/%s/repository/files/%s/raw", + url.QueryEscape(project), + url.PathEscape(fileName), + ) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var f bytes.Buffer + resp, err := s.client.Do(req, &f) + if err != nil { + return nil, resp, err + } + + return f.Bytes(), resp, err +} + // FileInfo represents file details of a GitLab repository file. // -// GitLab API docs: http://doc.gitlab.com/ce/api/repository_files.html +// GitLab API docs: https://docs.gitlab.com/ce/api/repository_files.html type FileInfo struct { - FilePath string `json:"file_path"` - BranchName string `json:"branch_name"` + FilePath string `json:"file_path"` + Branch string `json:"branch"` } func (r FileInfo) String() string { @@ -99,29 +192,32 @@ func (r FileInfo) String() string { // CreateFileOptions represents the available CreateFile() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repository_files.html#create-new-file-in-repository +// https://docs.gitlab.com/ce/api/repository_files.html#create-new-file-in-repository type CreateFileOptions struct { - FilePath string `url:"file_path,omitempty" json:"file_path,omitempty"` - BranchName string `url:"branch_name,omitempty" json:"branch_name,omitempty"` - Encoding string `url:"encoding,omitempty" json:"encoding,omitempty"` - Content string `url:"content,omitempty" json:"content,omitempty"` - CommitMessage string `url:"commit_message,omitempty" json:"commit_message,omitempty"` + Branch *string `url:"branch,omitempty" json:"branch,omitempty"` + Encoding *string `url:"encoding,omitempty" json:"encoding,omitempty"` + AuthorEmail *string `url:"author_email,omitempty" json:"author_email,omitempty"` + AuthorName *string `url:"author_name,omitempty" json:"author_name,omitempty"` + Content *string `url:"content,omitempty" json:"content,omitempty"` + CommitMessage *string `url:"commit_message,omitempty" json:"commit_message,omitempty"` } // CreateFile creates a new file in a repository. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repository_files.html#create-new-file-in-repository -func (s *RepositoryFilesService) CreateFile( - pid interface{}, - opt *CreateFileOptions) (*FileInfo, *Response, error) { +// https://docs.gitlab.com/ce/api/repository_files.html#create-new-file-in-repository +func (s *RepositoryFilesService) CreateFile(pid interface{}, fileName string, opt *CreateFileOptions, options ...OptionFunc) (*FileInfo, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/repository/files", url.QueryEscape(project)) + u := fmt.Sprintf( + "projects/%s/repository/files/%s", + url.QueryEscape(project), + url.PathEscape(fileName), + ) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -138,29 +234,33 @@ func (s *RepositoryFilesService) CreateFile( // UpdateFileOptions represents the available UpdateFile() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repository_files.html#update-existing-file-in-repository +// https://docs.gitlab.com/ce/api/repository_files.html#update-existing-file-in-repository type UpdateFileOptions struct { - FilePath string `url:"file_path,omitempty" json:"file_path,omitempty"` - BranchName string `url:"branch_name,omitempty" json:"branch_name,omitempty"` - Encoding string `url:"encoding,omitempty" json:"encoding,omitempty"` - Content string `url:"content,omitempty" json:"content,omitempty"` - CommitMessage string `url:"commit_message,omitempty" json:"commit_message,omitempty"` + Branch *string `url:"branch,omitempty" json:"branch,omitempty"` + Encoding *string `url:"encoding,omitempty" json:"encoding,omitempty"` + AuthorEmail *string `url:"author_email,omitempty" json:"author_email,omitempty"` + AuthorName *string `url:"author_name,omitempty" json:"author_name,omitempty"` + Content *string `url:"content,omitempty" json:"content,omitempty"` + CommitMessage *string `url:"commit_message,omitempty" json:"commit_message,omitempty"` + LastCommitID *string `url:"last_commit_id,omitempty" json:"last_commit_id,omitempty"` } // UpdateFile updates an existing file in a repository // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repository_files.html#update-existing-file-in-repository -func (s *RepositoryFilesService) UpdateFile( - pid interface{}, - opt *UpdateFileOptions) (*FileInfo, *Response, error) { +// https://docs.gitlab.com/ce/api/repository_files.html#update-existing-file-in-repository +func (s *RepositoryFilesService) UpdateFile(pid interface{}, fileName string, opt *UpdateFileOptions, options ...OptionFunc) (*FileInfo, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/repository/files", url.QueryEscape(project)) + u := fmt.Sprintf( + "projects/%s/repository/files/%s", + url.QueryEscape(project), + url.PathEscape(fileName), + ) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -177,36 +277,33 @@ func (s *RepositoryFilesService) UpdateFile( // DeleteFileOptions represents the available DeleteFile() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repository_files.html#delete-existing-file-in-repository +// https://docs.gitlab.com/ce/api/repository_files.html#delete-existing-file-in-repository type DeleteFileOptions struct { - FilePath string `url:"file_path,omitempty" json:"file_path,omitempty"` - BranchName string `url:"branch_name,omitempty" json:"branch_name,omitempty"` - CommitMessage string `url:"commit_message,omitempty" json:"commit_message,omitempty"` + Branch *string `url:"branch,omitempty" json:"branch,omitempty"` + AuthorEmail *string `url:"author_email,omitempty" json:"author_email,omitempty"` + AuthorName *string `url:"author_name,omitempty" json:"author_name,omitempty"` + CommitMessage *string `url:"commit_message,omitempty" json:"commit_message,omitempty"` } // DeleteFile deletes an existing file in a repository // // GitLab API docs: -// http://doc.gitlab.com/ce/api/repository_files.html#delete-existing-file-in-repository -func (s *RepositoryFilesService) DeleteFile( - pid interface{}, - opt *DeleteFileOptions) (*FileInfo, *Response, error) { +// https://docs.gitlab.com/ce/api/repository_files.html#delete-existing-file-in-repository +func (s *RepositoryFilesService) DeleteFile(pid interface{}, fileName string, opt *DeleteFileOptions, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { - return nil, nil, err - } - u := fmt.Sprintf("projects/%s/repository/files", url.QueryEscape(project)) - - req, err := s.client.NewRequest("DELETE", u, opt) - if err != nil { - return nil, nil, err + return nil, err } + u := fmt.Sprintf( + "projects/%s/repository/files/%s", + url.QueryEscape(project), + url.PathEscape(fileName), + ) - f := new(FileInfo) - resp, err := s.client.Do(req, f) + req, err := s.client.NewRequest("DELETE", u, opt, options) if err != nil { - return nil, resp, err + return nil, err } - return f, resp, err + return s.client.Do(req, nil) } diff --git a/api/runners.go b/api/runners.go new file mode 100644 index 0000000..960656c --- /dev/null +++ b/api/runners.go @@ -0,0 +1,410 @@ +// +// Copyright 2017, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net" + "net/url" + "time" +) + +// RunnersService handles communication with the runner related methods of the +// GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/runners.html +type RunnersService struct { + client *Client +} + +// Runner represents a GitLab CI Runner. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/runners.html +type Runner struct { + ID int `json:"id"` + Description string `json:"description"` + Active bool `json:"active"` + IsShared bool `json:"is_shared"` + IPAddress *net.IP `json:"ip_address"` + Name string `json:"name"` + Online bool `json:"online"` + Status string `json:"status"` + Token string `json:"token"` +} + +// RunnerDetails represents the GitLab CI runner details. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/runners.html +type RunnerDetails struct { + Active bool `json:"active"` + Architecture string `json:"architecture"` + Description string `json:"description"` + ID int `json:"id"` + IsShared bool `json:"is_shared"` + ContactedAt *time.Time `json:"contacted_at"` + Name string `json:"name"` + Online bool `json:"online"` + Status string `json:"status"` + Platform string `json:"platform"` + Projects []struct { + ID int `json:"id"` + Name string `json:"name"` + NameWithNamespace string `json:"name_with_namespace"` + Path string `json:"path"` + PathWithNamespace string `json:"path_with_namespace"` + } `json:"projects"` + Token string `json:"token"` + Revision string `json:"revision"` + TagList []string `json:"tag_list"` + Version string `json:"version"` + AccessLevel string `json:"access_level"` + MaximumTimeout int `json:"maximum_timeout"` +} + +// ListRunnersOptions represents the available ListRunners() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#list-owned-runners +type ListRunnersOptions struct { + ListOptions + Scope *string `url:"scope,omitempty" json:"scope,omitempty"` +} + +// ListRunners gets a list of runners accessible by the authenticated user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#list-owned-runners +func (s *RunnersService) ListRunners(opt *ListRunnersOptions, options ...OptionFunc) ([]*Runner, *Response, error) { + req, err := s.client.NewRequest("GET", "runners", opt, options) + if err != nil { + return nil, nil, err + } + + var rs []*Runner + resp, err := s.client.Do(req, &rs) + if err != nil { + return nil, resp, err + } + + return rs, resp, err +} + +// ListAllRunners gets a list of all runners in the GitLab instance. Access is +// restricted to users with admin privileges. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#list-all-runners +func (s *RunnersService) ListAllRunners(opt *ListRunnersOptions, options ...OptionFunc) ([]*Runner, *Response, error) { + req, err := s.client.NewRequest("GET", "runners/all", opt, options) + if err != nil { + return nil, nil, err + } + + var rs []*Runner + resp, err := s.client.Do(req, &rs) + if err != nil { + return nil, resp, err + } + + return rs, resp, err +} + +// GetRunnerDetails returns details for given runner. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#get-runner-39-s-details +func (s *RunnersService) GetRunnerDetails(rid interface{}, options ...OptionFunc) (*RunnerDetails, *Response, error) { + runner, err := parseID(rid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("runners/%s", runner) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var rs *RunnerDetails + resp, err := s.client.Do(req, &rs) + if err != nil { + return nil, resp, err + } + + return rs, resp, err +} + +// UpdateRunnerDetailsOptions represents the available UpdateRunnerDetails() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#update-runner-39-s-details +type UpdateRunnerDetailsOptions struct { + Description *string `url:"description,omitempty" json:"description,omitempty"` + Active *bool `url:"active,omitempty" json:"active,omitempty"` + TagList []string `url:"tag_list[],omitempty" json:"tag_list,omitempty"` + RunUntagged *bool `url:"run_untagged,omitempty" json:"run_untagged,omitempty"` + Locked *bool `url:"locked,omitempty" json:"locked,omitempty"` + AccessLevel *string `url:"access_level,omitempty" json:"access_level,omitempty"` + MaximumTimeout *int `url:"maximum_timeout,omitempty" json:"maximum_timeout,omitempty"` +} + +// UpdateRunnerDetails updates details for a given runner. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#update-runner-39-s-details +func (s *RunnersService) UpdateRunnerDetails(rid interface{}, opt *UpdateRunnerDetailsOptions, options ...OptionFunc) (*RunnerDetails, *Response, error) { + runner, err := parseID(rid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("runners/%s", runner) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + var rs *RunnerDetails + resp, err := s.client.Do(req, &rs) + if err != nil { + return nil, resp, err + } + + return rs, resp, err +} + +// RemoveRunner removes a runner. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#remove-a-runner +func (s *RunnersService) RemoveRunner(rid interface{}, options ...OptionFunc) (*Response, error) { + runner, err := parseID(rid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("runners/%s", runner) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// ListRunnerJobsOptions represents the available ListRunnerJobs() +// options. Status can be one of: running, success, failed, canceled. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#list-runner-39-s-jobs +type ListRunnerJobsOptions struct { + ListOptions + Status *string `url:"status,omitempty" json:"status,omitempty"` +} + +// ListRunnerJobs gets a list of jobs that are being processed or were processed by specified Runner. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#list-runner-39-s-jobs +func (s *RunnersService) ListRunnerJobs(rid interface{}, opt *ListRunnerJobsOptions, options ...OptionFunc) ([]*Job, *Response, error) { + runner, err := parseID(rid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("runners/%s/jobs", runner) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var rs []*Job + resp, err := s.client.Do(req, &rs) + if err != nil { + return nil, resp, err + } + + return rs, resp, err +} + +// ListProjectRunnersOptions represents the available ListProjectRunners() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#list-project-s-runners +type ListProjectRunnersOptions ListRunnersOptions + +// ListProjectRunners gets a list of runners accessible by the authenticated user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#list-project-s-runners +func (s *RunnersService) ListProjectRunners(pid interface{}, opt *ListProjectRunnersOptions, options ...OptionFunc) ([]*Runner, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/runners", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var rs []*Runner + resp, err := s.client.Do(req, &rs) + if err != nil { + return nil, resp, err + } + + return rs, resp, err +} + +// EnableProjectRunnerOptions represents the available EnableProjectRunner() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#enable-a-runner-in-project +type EnableProjectRunnerOptions struct { + RunnerID int `json:"runner_id"` +} + +// EnableProjectRunner enables an available specific runner in the project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#enable-a-runner-in-project +func (s *RunnersService) EnableProjectRunner(pid interface{}, opt *EnableProjectRunnerOptions, options ...OptionFunc) (*Runner, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/runners", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + var r *Runner + resp, err := s.client.Do(req, &r) + if err != nil { + return nil, resp, err + } + + return r, resp, err +} + +// DisableProjectRunner disables a specific runner from project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#disable-a-runner-from-project +func (s *RunnersService) DisableProjectRunner(pid interface{}, rid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + runner, err := parseID(rid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/runners/%s", url.QueryEscape(project), url.QueryEscape(runner)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// RegisterNewRunnerOptions represents the available RegisterNewRunner() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#register-a-new-runner +type RegisterNewRunnerOptions struct { + Token *string `url:"token" json:"token"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + Info *string `url:"info,omitempty" json:"info,omitempty"` + Active *bool `url:"active,omitempty" json:"active,omitempty"` + Locked *bool `url:"locked,omitempty" json:"locked,omitempty"` + RunUntagged *bool `url:"run_untagged,omitempty" json:"run_untagged,omitempty"` + TagList []string `url:"tag_list[],omitempty" json:"tag_list,omitempty"` + MaximumTimeout *int `url:"maximum_timeout,omitempty" json:"maximum_timeout,omitempty"` +} + +// RegisterNewRunner registers a new Runner for the instance. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#register-a-new-runner +func (s *RunnersService) RegisterNewRunner(opt *RegisterNewRunnerOptions, options ...OptionFunc) (*Runner, *Response, error) { + req, err := s.client.NewRequest("POST", "runners", opt, options) + if err != nil { + return nil, nil, err + } + + var r *Runner + resp, err := s.client.Do(req, &r) + if err != nil { + return nil, resp, err + } + + return r, resp, err +} + +// DeleteRegisteredRunnerOptions represents the available +// DeleteRegisteredRunner() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#delete-a-registered-runner +type DeleteRegisteredRunnerOptions struct { + Token *string `url:"token" json:"token"` +} + +// DeleteRegisteredRunner registers a new Runner for the instance. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#delete-a-registered-runner +func (s *RunnersService) DeleteRegisteredRunner(opt *DeleteRegisteredRunnerOptions, options ...OptionFunc) (*Response, error) { + req, err := s.client.NewRequest("DELETE", "runners", opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// VerifyRegisteredRunnerOptions represents the available +// VerifyRegisteredRunner() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#verify-authentication-for-a-registered-runner +type VerifyRegisteredRunnerOptions struct { + Token *string `url:"token" json:"token"` +} + +// VerifyRegisteredRunner registers a new Runner for the instance. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/runners.html#verify-authentication-for-a-registered-runner +func (s *RunnersService) VerifyRegisteredRunner(opt *VerifyRegisteredRunnerOptions, options ...OptionFunc) (*Response, error) { + req, err := s.client.NewRequest("POST", "runners/verify", opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/runners_test.go b/api/runners_test.go new file mode 100644 index 0000000..6dadd96 --- /dev/null +++ b/api/runners_test.go @@ -0,0 +1,267 @@ +// +// Copyright 2017, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" + "time" +) + +func TestDisableRunner(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/runners/2", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + _, err := client.Runners.DisableProjectRunner(1, 2, nil) + if err != nil { + t.Fatalf("Runners.DisableProjectRunner returns an error: %v", err) + } +} + +func TestListRunnersJobs(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/runners/1/jobs", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1},{"id":2}]`) + }) + + opt := &ListRunnerJobsOptions{} + + jobs, _, err := client.Runners.ListRunnerJobs(1, opt) + if err != nil { + t.Fatalf("Runners.ListRunnersJobs returns an error: %v", err) + } + + want := []*Job{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(want, jobs) { + t.Errorf("Runners.ListRunnersJobs returned %+v, want %+v", jobs, want) + } +} + +func TestRemoveRunner(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/runners/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + _, err := client.Runners.RemoveRunner(1, nil) + if err != nil { + t.Fatalf("Runners.RemoveARunner returns an error: %v", err) + } +} + +const exampleDetailRsp = `{ + "active": true, + "architecture": null, + "description": "test-1-20150125-test", + "id": 6, + "is_shared": false, + "contacted_at": "2016-01-25T16:39:48.066Z", + "name": null, + "online": true, + "status": "online", + "platform": null, + "projects": [ + { + "id": 1, + "name": "GitLab Community Edition", + "name_with_namespace": "GitLab.org / GitLab Community Edition", + "path": "gitlab-ce", + "path_with_namespace": "gitlab-org/gitlab-ce" + } + ], + "token": "205086a8e3b9a2b818ffac9b89d102", + "revision": null, + "tag_list": [ + "ruby", + "mysql" + ], + "version": null, + "access_level": "ref_protected", + "maximum_timeout": 3600 +}` + +func TestUpdateRunnersDetails(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/runners/6", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, exampleDetailRsp) + }) + + opt := &UpdateRunnerDetailsOptions{} + + details, _, err := client.Runners.UpdateRunnerDetails(6, opt, nil) + if err != nil { + t.Fatalf("Runners.UpdateRunnersDetails returns an error: %v", err) + } + + want := expectedParsedDetails() + if !reflect.DeepEqual(want, details) { + t.Errorf("Runners.UpdateRunnersDetails returned %+v, want %+v", details, want) + } +} + +func TestGetRunnerDetails(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/runners/6", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, exampleDetailRsp) + }) + + details, _, err := client.Runners.GetRunnerDetails(6, nil) + if err != nil { + t.Fatalf("Runners.GetRunnerDetails returns an error: %v", err) + } + + want := expectedParsedDetails() + if !reflect.DeepEqual(want, details) { + t.Errorf("Runners.UpdateRunnersDetails returned %+v, want %+v", details, want) + } +} + +// helper function returning expected result for string: &exampleDetailRsp +func expectedParsedDetails() *RunnerDetails { + proj := struct { + ID int `json:"id"` + Name string `json:"name"` + NameWithNamespace string `json:"name_with_namespace"` + Path string `json:"path"` + PathWithNamespace string `json:"path_with_namespace"` + }{ID: 1, Name: "GitLab Community Edition", NameWithNamespace: "GitLab.org / GitLab Community Edition", Path: "gitlab-ce", PathWithNamespace: "gitlab-org/gitlab-ce"} + timestamp, _ := time.Parse("2006-01-02T15:04:05.000Z", "2016-01-25T16:39:48.066Z") + return &RunnerDetails{ + Active: true, + Description: "test-1-20150125-test", + ID: 6, + IsShared: false, + ContactedAt: ×tamp, + Online: true, + Status: "online", + Token: "205086a8e3b9a2b818ffac9b89d102", + TagList: []string{"ruby", "mysql"}, + AccessLevel: "ref_protected", + Projects: []struct { + ID int `json:"id"` + Name string `json:"name"` + NameWithNamespace string `json:"name_with_namespace"` + Path string `json:"path"` + PathWithNamespace string `json:"path_with_namespace"` + }{proj}, + MaximumTimeout: 3600, + } +} + +// helper function returning expected result for string: &exampleRegisterNewRunner +func expectedParsedNewRunner() *Runner { + return &Runner{ + ID: 12345, + Token: "6337ff461c94fd3fa32ba3b1ff4125", + } +} + +const exampleRegisterNewRunner = `{ + "id": 12345, + "token": "6337ff461c94fd3fa32ba3b1ff4125" +}` + +func TestRegisterNewRunner(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/runners", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusCreated) + fmt.Fprint(w, exampleRegisterNewRunner) + }) + + opt := &RegisterNewRunnerOptions{} + + runner, resp, err := client.Runners.RegisterNewRunner(opt, nil) + if err != nil { + t.Fatalf("Runners.RegisterNewRunner returns an error: %v", err) + } + + want := expectedParsedNewRunner() + if !reflect.DeepEqual(want, runner) { + t.Errorf("Runners.RegisterNewRunner returned %+v, want %+v", runner, want) + } + + wantCode := 201 + if !reflect.DeepEqual(wantCode, resp.StatusCode) { + t.Errorf("Runners.DeleteRegisteredRunner returned status code %+v, want %+v", resp.StatusCode, wantCode) + } +} + +func TestDeleteRegisteredRunner(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/runners", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + opt := &DeleteRegisteredRunnerOptions{} + + resp, err := client.Runners.DeleteRegisteredRunner(opt, nil) + if err != nil { + t.Fatalf("Runners.DeleteRegisteredRunner returns an error: %v", err) + } + + want := 204 + if !reflect.DeepEqual(want, resp.StatusCode) { + t.Errorf("Runners.DeleteRegisteredRunner returned returned status code %+v, want %+v", resp.StatusCode, want) + } +} + +func TestVerifyRegisteredRunner(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/runners/verify", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusOK) + }) + + opt := &VerifyRegisteredRunnerOptions{} + + resp, err := client.Runners.VerifyRegisteredRunner(opt, nil) + if err != nil { + t.Fatalf("Runners.VerifyRegisteredRunner returns an error: %v", err) + } + + want := 200 + if !reflect.DeepEqual(want, resp.StatusCode) { + t.Errorf("Runners.VerifyRegisteredRunner returned returned status code %+v, want %+v", resp.StatusCode, want) + } +} diff --git a/api/search.go b/api/search.go new file mode 100644 index 0000000..99c2f02 --- /dev/null +++ b/api/search.go @@ -0,0 +1,326 @@ +// +// Copyright 2018, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// SearchService handles communication with the search related methods of the +// GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html +type SearchService struct { + client *Client +} + +// SearchOptions represents the available options for all search methods. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html +type SearchOptions ListOptions + +type searchOptions struct { + SearchOptions + Scope string `url:"scope" json:"scope"` + Search string `url:"search" json:"search"` +} + +// Projects searches the expression within projects +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-projects +func (s *SearchService) Projects(query string, opt *SearchOptions, options ...OptionFunc) ([]*Project, *Response, error) { + var ps []*Project + resp, err := s.search("projects", query, &ps, opt, options...) + return ps, resp, err +} + +// ProjectsByGroup searches the expression within projects for +// the specified group +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#group-search-api +func (s *SearchService) ProjectsByGroup(gid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Project, *Response, error) { + var ps []*Project + resp, err := s.searchByGroup(gid, "projects", query, &ps, opt, options...) + return ps, resp, err +} + +// Issues searches the expression within issues +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-issues +func (s *SearchService) Issues(query string, opt *SearchOptions, options ...OptionFunc) ([]*Issue, *Response, error) { + var is []*Issue + resp, err := s.search("issues", query, &is, opt, options...) + return is, resp, err +} + +// IssuesByGroup searches the expression within issues for +// the specified group +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-issues +func (s *SearchService) IssuesByGroup(gid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Issue, *Response, error) { + var is []*Issue + resp, err := s.searchByGroup(gid, "issues", query, &is, opt, options...) + return is, resp, err +} + +// IssuesByProject searches the expression within issues for +// the specified project +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-issues +func (s *SearchService) IssuesByProject(pid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Issue, *Response, error) { + var is []*Issue + resp, err := s.searchByProject(pid, "issues", query, &is, opt, options...) + return is, resp, err +} + +// MergeRequests searches the expression within merge requests +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/search.html#scope-merge_requests +func (s *SearchService) MergeRequests(query string, opt *SearchOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) { + var ms []*MergeRequest + resp, err := s.search("merge_requests", query, &ms, opt, options...) + return ms, resp, err +} + +// MergeRequestsByGroup searches the expression within merge requests for +// the specified group +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/search.html#scope-merge_requests +func (s *SearchService) MergeRequestsByGroup(gid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) { + var ms []*MergeRequest + resp, err := s.searchByGroup(gid, "merge_requests", query, &ms, opt, options...) + return ms, resp, err +} + +// MergeRequestsByProject searches the expression within merge requests for +// the specified project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/search.html#scope-merge_requests +func (s *SearchService) MergeRequestsByProject(pid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) { + var ms []*MergeRequest + resp, err := s.searchByProject(pid, "merge_requests", query, &ms, opt, options...) + return ms, resp, err +} + +// Milestones searches the expression within milestones +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-milestones +func (s *SearchService) Milestones(query string, opt *SearchOptions, options ...OptionFunc) ([]*Milestone, *Response, error) { + var ms []*Milestone + resp, err := s.search("milestones", query, &ms, opt, options...) + return ms, resp, err +} + +// MilestonesByGroup searches the expression within milestones for +// the specified group +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-milestones +func (s *SearchService) MilestonesByGroup(gid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Milestone, *Response, error) { + var ms []*Milestone + resp, err := s.searchByGroup(gid, "milestones", query, &ms, opt, options...) + return ms, resp, err +} + +// MilestonesByProject searches the expression within milestones for +// the specified project +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-milestones +func (s *SearchService) MilestonesByProject(pid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Milestone, *Response, error) { + var ms []*Milestone + resp, err := s.searchByProject(pid, "milestones", query, &ms, opt, options...) + return ms, resp, err +} + +// SnippetTitles searches the expression within snippet titles +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/search.html#scope-snippet_titles +func (s *SearchService) SnippetTitles(query string, opt *SearchOptions, options ...OptionFunc) ([]*Snippet, *Response, error) { + var ss []*Snippet + resp, err := s.search("snippet_titles", query, &ss, opt, options...) + return ss, resp, err +} + +// SnippetBlobs searches the expression within snippet blobs +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/search.html#scope-snippet_blobs +func (s *SearchService) SnippetBlobs(query string, opt *SearchOptions, options ...OptionFunc) ([]*Snippet, *Response, error) { + var ss []*Snippet + resp, err := s.search("snippet_blobs", query, &ss, opt, options...) + return ss, resp, err +} + +// NotesByProject searches the expression within notes for the specified +// project +// +// GitLab API docs: // https://docs.gitlab.com/ce/api/search.html#scope-notes +func (s *SearchService) NotesByProject(pid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Note, *Response, error) { + var ns []*Note + resp, err := s.searchByProject(pid, "notes", query, &ns, opt, options...) + return ns, resp, err +} + +// WikiBlobs searches the expression within all wiki blobs +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/search.html#scope-wiki_blobs +func (s *SearchService) WikiBlobs(query string, opt *SearchOptions, options ...OptionFunc) ([]*Wiki, *Response, error) { + var ws []*Wiki + resp, err := s.search("wiki_blobs", query, &ws, opt, options...) + return ws, resp, err +} + +// WikiBlobsByGroup searches the expression within wiki blobs for +// specified group +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/search.html#scope-wiki_blobs +func (s *SearchService) WikiBlobsByGroup(gid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Wiki, *Response, error) { + var ws []*Wiki + resp, err := s.searchByGroup(gid, "wiki_blobs", query, &ws, opt, options...) + return ws, resp, err +} + +// WikiBlobsByProject searches the expression within wiki blobs for +// the specified project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/search.html#scope-wiki_blobs +func (s *SearchService) WikiBlobsByProject(pid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Wiki, *Response, error) { + var ws []*Wiki + resp, err := s.searchByProject(pid, "wiki_blobs", query, &ws, opt, options...) + return ws, resp, err +} + +// Commits searches the expression within all commits +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-commits +func (s *SearchService) Commits(query string, opt *SearchOptions, options ...OptionFunc) ([]*Commit, *Response, error) { + var cs []*Commit + resp, err := s.search("commits", query, &cs, opt, options...) + return cs, resp, err +} + +// CommitsByGroup searches the expression within commits for the specified +// group +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-commits +func (s *SearchService) CommitsByGroup(gid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Commit, *Response, error) { + var cs []*Commit + resp, err := s.searchByGroup(gid, "commits", query, &cs, opt, options...) + return cs, resp, err +} + +// CommitsByProject searches the expression within commits for the +// specified project +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-commits +func (s *SearchService) CommitsByProject(pid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Commit, *Response, error) { + var cs []*Commit + resp, err := s.searchByProject(pid, "commits", query, &cs, opt, options...) + return cs, resp, err +} + +// Blob represents a single blob. +type Blob struct { + Basename string `json:"basename"` + Data string `json:"data"` + Filename string `json:"filename"` + ID int `json:"id"` + Ref string `json:"ref"` + Startline int `json:"startline"` + ProjectID int `json:"project_id"` +} + +// Blobs searches the expression within all blobs +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-blobs +func (s *SearchService) Blobs(query string, opt *SearchOptions, options ...OptionFunc) ([]*Blob, *Response, error) { + var bs []*Blob + resp, err := s.search("blobs", query, &bs, opt, options...) + return bs, resp, err +} + +// BlobsByGroup searches the expression within blobs for the specified +// group +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-blobs +func (s *SearchService) BlobsByGroup(gid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Blob, *Response, error) { + var bs []*Blob + resp, err := s.searchByGroup(gid, "blobs", query, &bs, opt, options...) + return bs, resp, err +} + +// BlobsByProject searches the expression within blobs for the specified +// project +// +// GitLab API docs: https://docs.gitlab.com/ce/api/search.html#scope-blobs +func (s *SearchService) BlobsByProject(pid interface{}, query string, opt *SearchOptions, options ...OptionFunc) ([]*Blob, *Response, error) { + var bs []*Blob + resp, err := s.searchByProject(pid, "blobs", query, &bs, opt, options...) + return bs, resp, err +} + +func (s *SearchService) search(scope, query string, result interface{}, opt *SearchOptions, options ...OptionFunc) (*Response, error) { + opts := &searchOptions{SearchOptions: *opt, Scope: scope, Search: query} + + req, err := s.client.NewRequest("GET", "search", opts, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, result) +} + +func (s *SearchService) searchByGroup(gid interface{}, scope, query string, result interface{}, opt *SearchOptions, options ...OptionFunc) (*Response, error) { + group, err := parseID(gid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("groups/%s/-/search", url.QueryEscape(group)) + + opts := &searchOptions{SearchOptions: *opt, Scope: scope, Search: query} + + req, err := s.client.NewRequest("GET", u, opts, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, result) +} + +func (s *SearchService) searchByProject(pid interface{}, scope, query string, result interface{}, opt *SearchOptions, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/-/search", url.QueryEscape(project)) + + opts := &searchOptions{SearchOptions: *opt, Scope: scope, Search: query} + + req, err := s.client.NewRequest("GET", u, opts, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, result) +} diff --git a/api/services.go b/api/services.go index 1076e6a..313568f 100644 --- a/api/services.go +++ b/api/services.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,239 +25,625 @@ import ( // ServicesService handles communication with the services related methods of // the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/services.html +// GitLab API docs: https://docs.gitlab.com/ce/api/services.html type ServicesService struct { client *Client } +// Service represents a GitLab service. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/services.html type Service struct { - ID *int `json:"id"` - Title *string `json:"title"` - CreatedAt *time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"created_at"` - Active *bool `json:"active"` - 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"` + ID int `json:"id"` + Title string `json:"title"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + Active bool `json:"active"` + PushEvents bool `json:"push_events"` + IssuesEvents bool `json:"issues_events"` + ConfidentialIssuesEvents bool `json:"confidential_issues_events"` + MergeRequestsEvents bool `json:"merge_requests_events"` + TagPushEvents bool `json:"tag_push_events"` + NoteEvents bool `json:"note_events"` + ConfidentialNoteEvents bool `json:"confidential_note_events"` + PipelineEvents bool `json:"pipeline_events"` + JobEvents bool `json:"job_events"` + WikiPageEvents bool `json:"wiki_page_events"` } // SetGitLabCIServiceOptions represents the available SetGitLabCIService() // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/services.html#edit-gitlab-ci-service +// https://docs.gitlab.com/ce/api/services.html#edit-gitlab-ci-service type SetGitLabCIServiceOptions struct { - Token string `url:"token,omitempty" json:"token,omitempty"` - ProjectURL string `url:"project_url,omitempty" json:"project_url,omitempty"` + Token *string `url:"token,omitempty" json:"token,omitempty"` + ProjectURL *string `url:"project_url,omitempty" json:"project_url,omitempty"` } // SetGitLabCIService sets GitLab CI service for a project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/services.html#edit-gitlab-ci-service -func (s *ServicesService) SetGitLabCIService( - pid interface{}, - opt *SetGitLabCIServiceOptions) (*Response, error) { +// https://docs.gitlab.com/ce/api/services.html#edit-gitlab-ci-service +func (s *ServicesService) SetGitLabCIService(pid interface{}, opt *SetGitLabCIServiceOptions, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } u := fmt.Sprintf("projects/%s/services/gitlab-ci", url.QueryEscape(project)) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) - if err != nil { - return resp, err - } - - return resp, err + return s.client.Do(req, nil) } // DeleteGitLabCIService deletes GitLab CI service settings for a project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/services.html#delete-gitlab-ci-service -func (s *ServicesService) DeleteGitLabCIService(pid interface{}) (*Response, error) { +// https://docs.gitlab.com/ce/api/services.html#delete-gitlab-ci-service +func (s *ServicesService) DeleteGitLabCIService(pid interface{}, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } u := fmt.Sprintf("projects/%s/services/gitlab-ci", url.QueryEscape(project)) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) - if err != nil { - return resp, err - } - - return resp, err + return s.client.Do(req, nil) } // SetHipChatServiceOptions represents the available SetHipChatService() // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/services.html#edit-hipchat-service +// https://docs.gitlab.com/ce/api/services.html#edit-hipchat-service type SetHipChatServiceOptions struct { - Token string `url:"token,omitempty" json:"token,omitempty" ` - Room string `url:"room,omitempty" json:"room,omitempty"` + Token *string `url:"token,omitempty" json:"token,omitempty" ` + Room *string `url:"room,omitempty" json:"room,omitempty"` } // SetHipChatService sets HipChat service for a project // // GitLab API docs: -// http://doc.gitlab.com/ce/api/services.html#edit-hipchat-service -func (s *ServicesService) SetHipChatService( - pid interface{}, - opt *SetHipChatServiceOptions) (*Response, error) { +// https://docs.gitlab.com/ce/api/services.html#edit-hipchat-service +func (s *ServicesService) SetHipChatService(pid interface{}, opt *SetHipChatServiceOptions, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } u := fmt.Sprintf("projects/%s/services/hipchat", url.QueryEscape(project)) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) - if err != nil { - return resp, err - } - - return resp, err + return s.client.Do(req, nil) } // DeleteHipChatService deletes HipChat service for project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/services.html#delete-hipchat-service -func (s *ServicesService) DeleteHipChatService(pid interface{}) (*Response, error) { +// https://docs.gitlab.com/ce/api/services.html#delete-hipchat-service +func (s *ServicesService) DeleteHipChatService(pid interface{}, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } u := fmt.Sprintf("projects/%s/services/hipchat", url.QueryEscape(project)) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + return s.client.Do(req, nil) +} + +// DroneCIService represents Drone CI service settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#drone-ci +type DroneCIService struct { + Service + Properties *DroneCIServiceProperties `json:"properties"` +} + +// DroneCIServiceProperties represents Drone CI specific properties. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#drone-ci +type DroneCIServiceProperties struct { + Token string `json:"token"` + DroneURL string `json:"drone_url"` + EnableSSLVerification bool `json:"enable_ssl_verification"` +} + +// GetDroneCIService gets Drone CI service settings for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#get-drone-ci-service-settings +func (s *ServicesService) GetDroneCIService(pid interface{}, options ...OptionFunc) (*DroneCIService, *Response, error) { + project, err := parseID(pid) if err != nil { - return resp, err + return nil, nil, err } + u := fmt.Sprintf("projects/%s/services/drone-ci", url.QueryEscape(project)) - return resp, err + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + svc := new(DroneCIService) + resp, err := s.client.Do(req, svc) + if err != nil { + return nil, resp, err + } + + return svc, resp, err } // SetDroneCIServiceOptions represents the available SetDroneCIService() // options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/services.html#createedit-drone-ci-service +// https://docs.gitlab.com/ce/api/services.html#createedit-drone-ci-service type SetDroneCIServiceOptions struct { - Token string `url:"token" json:"token" ` - DroneURL string `url:"drone_url" json:"drone_url"` - EnableSSLVerification string `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` + Token *string `url:"token" json:"token" ` + DroneURL *string `url:"drone_url" json:"drone_url"` + EnableSSLVerification *bool `url:"enable_ssl_verification,omitempty" json:"enable_ssl_verification,omitempty"` } // SetDroneCIService sets Drone CI service for a project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/services.html#createedit-drone-ci-service -func (s *ServicesService) SetDroneCIService( - pid interface{}, - opt *SetDroneCIServiceOptions) (*Response, error) { +// https://docs.gitlab.com/ce/api/services.html#createedit-drone-ci-service +func (s *ServicesService) SetDroneCIService(pid interface{}, opt *SetDroneCIServiceOptions, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } u := fmt.Sprintf("projects/%s/services/drone-ci", url.QueryEscape(project)) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + return s.client.Do(req, nil) +} + +// DeleteDroneCIService deletes Drone CI service settings for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#delete-drone-ci-service +func (s *ServicesService) DeleteDroneCIService(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) if err != nil { - return resp, err + return nil, err + } + u := fmt.Sprintf("projects/%s/services/drone-ci", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err } - return resp, err + return s.client.Do(req, nil) } -// DeleteDroneCIService deletes Drone CI service settings for a project. +// SlackService represents Slack service settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#slack +type SlackService struct { + Service + Properties *SlackServiceProperties `json:"properties"` +} + +// SlackServiceProperties represents Slack specific properties. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#slack +type SlackServiceProperties struct { + // Note: NotifyOnlyBrokenPipelines and NotifyOnlyDefaultBranch are not + // just "bool" because in some cases gitlab returns + // "notify_only_broken_pipelines": true, and in other cases + // "notify_only_broken_pipelines": "1". The same is for + // "notify_only_default_branch" field. + // We need to handle this, until the bug will be fixed. + // Ref: https://gitlab.com/gitlab-org/gitlab-ce/issues/50122 + + NotifyOnlyBrokenPipelines BoolValue `url:"notify_only_broken_pipelines,omitempty" json:"notify_only_broken_pipelines,omitempty"` + NotifyOnlyDefaultBranch BoolValue `url:"notify_only_default_branch,omitempty" json:"notify_only_default_branch,omitempty"` + WebHook string `url:"webhook,omitempty" json:"webhook,omitempty"` + Username string `url:"username,omitempty" json:"username,omitempty"` + Channel string `url:"channel,omitempty" json:"channel,omitempty"` + PushChannel string `url:"push_channel,omitempty" json:"push_channel,omitempty"` + IssueChannel string `url:"issue_channel,omitempty" json:"issue_channel,omitempty"` + ConfidentialIssueChannel string `url:"confidential_issue_channel,omitempty" json:"confidential_issue_channel,omitempty"` + MergeRequestChannel string `url:"merge_request_channel,omitempty" json:"merge_request_channel,omitempty"` + NoteChannel string `url:"note_channel,omitempty" json:"note_channel,omitempty"` + ConfidentialNoteChannel string `url:"confidential_note_channel,omitempty" json:"confidential_note_channel,omitempty"` + TagPushChannel string `url:"tag_push_channel,omitempty" json:"tag_push_channel,omitempty"` + PipelineChannel string `url:"pipeline_channel,omitempty" json:"pipeline_channel,omitempty"` + WikiPageChannel string `url:"wiki_page_channel,omitempty" json:"wiki_page_channel,omitempty"` +} + +// GetSlackService gets Slack service settings for a project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/services.html#delete-drone-ci-service -func (s *ServicesService) DeleteDroneCIService(pid interface{}) (*Response, error) { +// https://docs.gitlab.com/ce/api/services.html#get-slack-service-settings +func (s *ServicesService) GetSlackService(pid interface{}, options ...OptionFunc) (*SlackService, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/services/slack", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + svc := new(SlackService) + resp, err := s.client.Do(req, svc) + if err != nil { + return nil, resp, err + } + + return svc, resp, err +} + +// SetSlackServiceOptions represents the available SetSlackService() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#edit-slack-service +type SetSlackServiceOptions struct { + WebHook *string `url:"webhook,omitempty" json:"webhook,omitempty"` + Username *string `url:"username,omitempty" json:"username,omitempty"` + Channel *string `url:"channel,omitempty" json:"channel,omitempty"` + NotifyOnlyBrokenPipelines *bool `url:"notify_only_broken_pipelines,omitempty" json:"notify_only_broken_pipelines,omitempty"` + NotifyOnlyDefaultBranch *bool `url:"notify_only_default_branch,omitempty" json:"notify_only_default_branch,omitempty"` + PushEvents *bool `url:"push_events,omitempty" json:"push_events,omitempty"` + PushChannel *string `url:"push_channel,omitempty" json:"push_channel,omitempty"` + IssuesEvents *bool `url:"issues_events,omitempty" json:"issues_events,omitempty"` + IssueChannel *string `url:"issue_channel,omitempty" json:"issue_channel,omitempty"` + ConfidentialIssuesEvents *bool `url:"confidential_issues_events,omitempty" json:"confidential_issues_events,omitempty"` + ConfidentialIssueChannel *string `url:"confidential_issue_channel,omitempty" json:"confidential_issue_channel,omitempty"` + MergeRequestsEvents *bool `url:"merge_requests_events,omitempty" json:"merge_requests_events,omitempty"` + MergeRequestChannel *string `url:"merge_request_channel,omitempty" json:"merge_request_channel,omitempty"` + TagPushEvents *bool `url:"tag_push_events,omitempty" json:"tag_push_events,omitempty"` + TagPushChannel *string `url:"tag_push_channel,omitempty" json:"tag_push_channel,omitempty"` + NoteEvents *bool `url:"note_events,omitempty" json:"note_events,omitempty"` + NoteChannel *string `url:"note_channel,omitempty" json:"note_channel,omitempty"` + ConfidentialNoteEvents *bool `url:"confidential_note_events" json:"confidential_note_events"` + // TODO: Currently, GitLab ignores this option (not implemented yet?), so + // there is no way to set it. Uncomment when this is fixed. + // See: https://gitlab.com/gitlab-org/gitlab-ce/issues/49730 + //ConfidentialNoteChannel *string `json:"confidential_note_channel,omitempty"` + PipelineEvents *bool `url:"pipeline_events,omitempty" json:"pipeline_events,omitempty"` + PipelineChannel *string `url:"pipeline_channel,omitempty" json:"pipeline_channel,omitempty"` + WikiPageChannel *string `url:"wiki_page_channel,omitempty" json:"wiki_page_channel,omitempty"` + WikiPageEvents *bool `url:"wiki_page_events" json:"wiki_page_events"` +} + +// SetSlackService sets Slack service for a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#edit-slack-service +func (s *ServicesService) SetSlackService(pid interface{}, opt *SetSlackServiceOptions, options ...OptionFunc) (*Response, error) { project, err := parseID(pid) if err != nil { return nil, err } - u := fmt.Sprintf("projects/%s/services/drone-ci", url.QueryEscape(project)) + u := fmt.Sprintf("projects/%s/services/slack", url.QueryEscape(project)) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + return s.client.Do(req, nil) +} + +// DeleteSlackService deletes Slack service for project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#delete-slack-service +func (s *ServicesService) DeleteSlackService(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) if err != nil { - return resp, err + return nil, err } + u := fmt.Sprintf("projects/%s/services/slack", url.QueryEscape(project)) - return resp, err + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) } -// DroneCIServiceProperties represents Drone CI specific properties. -type DroneCIServiceProperties struct { - Token *string `url:"token" json:"token"` - DroneURL *string `url:"drone_url" json:"drone_url"` - EnableSSLVerification *string `url:"enable_ssl_verification" json:"enable_ssl_verification"` +// JiraService represents Jira service settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#jira +type JiraService struct { + Service + Properties *JiraServiceProperties `json:"properties"` } -// DroneCIService represents Drone CI service settings. -type DroneCIService struct { +// JiraServiceProperties represents Jira specific properties. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#jira +type JiraServiceProperties struct { + URL *string `url:"url,omitempty" json:"url,omitempty"` + ProjectKey *string `url:"project_key,omitempty" json:"project_key,omitempty" ` + Username *string `url:"username,omitempty" json:"username,omitempty" ` + Password *string `url:"password,omitempty" json:"password,omitempty" ` + JiraIssueTransitionID *int `url:"jira_issue_transition_id,omitempty" json:"jira_issue_transition_id,omitempty"` +} + +// GetJiraService gets Jira service settings for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#get-jira-service-settings +func (s *ServicesService) GetJiraService(pid interface{}, options ...OptionFunc) (*JiraService, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/services/jira", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + svc := new(JiraService) + resp, err := s.client.Do(req, svc) + if err != nil { + return nil, resp, err + } + + return svc, resp, err +} + +// SetJiraServiceOptions represents the available SetJiraService() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#edit-jira-service +type SetJiraServiceOptions JiraServiceProperties + +// SetJiraService sets Jira service for a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#edit-jira-service +func (s *ServicesService) SetJiraService(pid interface{}, opt *SetJiraServiceOptions, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/jira", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteJiraService deletes Jira service for project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#delete-jira-service +func (s *ServicesService) DeleteJiraService(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/jira", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// JenkinsCIService represents Jenkins CI service settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/services.html#jenkins-ci +type JenkinsCIService struct { Service - Properties *DroneCIServiceProperties `json:"properties"` + Properties *JenkinsCIServiceProperties `json:"properties"` } -// GetDroneCIService gets Drone CI service settings for a project. +// JenkinsCIServiceProperties represents Jenkins CI specific properties. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/services.html#jenkins-ci +type JenkinsCIServiceProperties struct { + URL *string `url:"jenkins_url,omitempty" json:"jenkins_url,omitempty"` + ProjectName *string `url:"project_name,omitempty" json:"project_name,omitempty"` + Username *string `url:"username,omitempty" json:"username,omitempty"` +} + +// GetJenkinsCIService gets Jenkins CI service settings for a project. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/services.html#get-drone-ci-service-settings -func (s *ServicesService) GetDroneCIService(pid interface{}) (*DroneCIService, *Response, error) { +// https://docs.gitlab.com/ee/api/services.html#get-jenkins-ci-service-settings +func (s *ServicesService) GetJenkinsCIService(pid interface{}, options ...OptionFunc) (*JenkinsCIService, *Response, error) { project, err := parseID(pid) if err != nil { return nil, nil, err } - u := fmt.Sprintf("projects/%s/services/drone-ci", url.QueryEscape(project)) + u := fmt.Sprintf("projects/%s/services/jenkins", url.QueryEscape(project)) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } - opt := new(DroneCIService) - resp, err := s.client.Do(req, opt) + svc := new(JenkinsCIService) + resp, err := s.client.Do(req, svc) if err != nil { return nil, resp, err } - return opt, resp, err + return svc, resp, err +} + +// SetJenkinsCIServiceOptions represents the available SetJenkinsCIService() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/services.html#jenkins-ci +type SetJenkinsCIServiceOptions struct { + URL *string `url:"jenkins_url,omitempty" json:"jenkins_url,omitempty"` + ProjectName *string `url:"project_name,omitempty" json:"project_name,omitempty"` + Username *string `url:"username,omitempty" json:"username,omitempty"` + Password *string `url:"password,omitempty" json:"password,omitempty"` +} + +// SetJenkinsCIService sets Jenkins service for a project +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/services.html#create-edit-jenkins-ci-service +func (s *ServicesService) SetJenkinsCIService(pid interface{}, opt *SetJenkinsCIServiceOptions, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/jenkins", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteJenkinsCIService deletes Jenkins CI service for project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#delete-jira-service +func (s *ServicesService) DeleteJenkinsCIService(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/jenkins", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// MicrosoftTeamsService represents Microsoft Teams service settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#microsoft-teams +type MicrosoftTeamsService struct { + Service + Properties *MicrosoftTeamsServiceProperties `json:"properties"` +} + +// MicrosoftTeamsServiceProperties represents Microsoft Teams specific properties. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#microsoft-teams +type MicrosoftTeamsServiceProperties struct { + WebHook string `json:"webhook"` +} + +// GetMicrosoftTeamsService gets MicrosoftTeams service settings for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#get-microsoft-teams-service-settings +func (s *ServicesService) GetMicrosoftTeamsService(pid interface{}, options ...OptionFunc) (*MicrosoftTeamsService, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/services/microsoft-teams", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + svc := new(MicrosoftTeamsService) + resp, err := s.client.Do(req, svc) + if err != nil { + return nil, resp, err + } + + return svc, resp, err +} + +// SetMicrosoftTeamsServiceOptions represents the available SetMicrosoftTeamsService() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#create-edit-microsoft-teams-service +type SetMicrosoftTeamsServiceOptions struct { + WebHook *string `url:"webhook,omitempty" json:"webhook,omitempty"` +} + +// SetMicrosoftTeamsService sets Microsoft Teams service for a project +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#create-edit-microsoft-teams-service +func (s *ServicesService) SetMicrosoftTeamsService(pid interface{}, opt *SetMicrosoftTeamsServiceOptions, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/microsoft-teams", url.QueryEscape(project)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, err + } + return s.client.Do(req, nil) +} + +// DeleteMicrosoftTeamsService deletes Microsoft Teams service for project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/services.html#delete-microsoft-teams-service +func (s *ServicesService) DeleteMicrosoftTeamsService(pid interface{}, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/microsoft-teams", url.QueryEscape(project)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) } diff --git a/api/services_test.go b/api/services_test.go index 58ee418..853b5d1 100644 --- a/api/services_test.go +++ b/api/services_test.go @@ -7,22 +7,36 @@ import ( "testing" ) +func TestGetDroneCIService(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/services/drone-ci", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":1}`) + }) + want := &DroneCIService{Service: Service{ID: 1}} + + service, _, err := client.Services.GetDroneCIService(1) + if err != nil { + t.Fatalf("Services.GetDroneCIService returns an error: %v", err) + } + if !reflect.DeepEqual(want, service) { + t.Errorf("Services.GetDroneCIService returned %+v, want %+v", service, want) + } +} + func TestSetDroneCIService(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects/1/services/drone-ci", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects/1/services/drone-ci", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "PUT") - testJsonBody(t, r, values{ - "token": "t", - "drone_url": "u", - "enable_ssl_verification": "true", - }) }) - opt := &SetDroneCIServiceOptions{"t", "u", "true"} - _, err := client.Services.SetDroneCIService(1, opt) + opt := &SetDroneCIServiceOptions{String("t"), String("u"), Bool(true)} + _, err := client.Services.SetDroneCIService(1, opt) if err != nil { t.Fatalf("Services.SetDroneCIService returns an error: %v", err) } @@ -32,34 +46,120 @@ func TestDeleteDroneCIService(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects/1/services/drone-ci", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects/1/services/drone-ci", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "DELETE") }) _, err := client.Services.DeleteDroneCIService(1) - if err != nil { t.Fatalf("Services.DeleteDroneCIService returns an error: %v", err) } } -func TestGetDroneCIService(t *testing.T) { +func TestGetSlackService(t *testing.T) { mux, server, client := setup() defer teardown(server) - mux.HandleFunc("/projects/1/services/drone-ci", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/api/v4/projects/1/services/slack", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{"id":1}`) }) - want := &DroneCIService{Service: Service{ID: Int(1)}} + want := &SlackService{Service: Service{ID: 1}} - service, _, err := client.Services.GetDroneCIService(1) + service, _, err := client.Services.GetSlackService(1) + if err != nil { + t.Fatalf("Services.GetSlackService returns an error: %v", err) + } + if !reflect.DeepEqual(want, service) { + t.Errorf("Services.GetSlackService returned %+v, want %+v", service, want) + } +} + +func TestSetSlackService(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + mux.HandleFunc("/api/v4/projects/1/services/slack", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + }) + + opt := &SetSlackServiceOptions{ + WebHook: String("webhook_uri"), + Username: String("username"), + Channel: String("#development"), + } + + _, err := client.Services.SetSlackService(1, opt) if err != nil { - t.Fatalf("Services.GetDroneCIService returns an error: %v", err) + t.Fatalf("Services.SetSlackService returns an error: %v", err) } +} + +func TestDeleteSlackService(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + mux.HandleFunc("/api/v4/projects/1/services/slack", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Services.DeleteSlackService(1) + if err != nil { + t.Fatalf("Services.DeleteSlackService returns an error: %v", err) + } +} + +func TestGetJiraService(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/services/jira", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":1}`) + }) + want := &JiraService{Service: Service{ID: 1}} + + service, _, err := client.Services.GetJiraService(1) + if err != nil { + t.Fatalf("Services.GetJiraService returns an error: %v", err) + } if !reflect.DeepEqual(want, service) { - t.Errorf("Services.GetDroneCIService returned %+v, want %+v", service, want) + t.Errorf("Services.GetJiraService returned %+v, want %+v", service, want) + } +} + +func TestSetJiraService(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/services/jira", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + }) + + opt := &SetJiraServiceOptions{ + URL: String("asd"), + ProjectKey: String("as"), + Username: String("aas"), + Password: String("asd"), + JiraIssueTransitionID: Int(2), + } + + _, err := client.Services.SetJiraService(1, opt) + if err != nil { + t.Fatalf("Services.SetJiraService returns an error: %v", err) + } +} + +func TestDeleteJiraService(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/services/jira", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Services.DeleteJiraService(1) + if err != nil { + t.Fatalf("Services.DeleteJiraService returns an error: %v", err) } } diff --git a/api/session.go b/api/session.go deleted file mode 100644 index ce7dcba..0000000 --- a/api/session.go +++ /dev/null @@ -1,78 +0,0 @@ -// -// Copyright 2015, Sander van Harmelen -// -// 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 gitlab - -import "time" - -// SessionService handles communication with the session related methods of -// the GitLab API. -// -// GitLab API docs: http://doc.gitlab.com/ce/api/session.html -type SessionService struct { - client *Client -} - -// Session represents a GitLab session. -// -// GitLab API docs: http://doc.gitlab.com/ce/api/session.html#session -type Session struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - PrivateToken string `json:"private_token"` - Blocked bool `json:"blocked"` - CreatedAt time.Time `json:"created_at"` - Bio interface{} `json:"bio"` - Skype string `json:"skype"` - Linkedin string `json:"linkedin"` - Twitter string `json:"twitter"` - WebsiteURL string `json:"website_url"` - DarkScheme bool `json:"dark_scheme"` - ThemeID int `json:"theme_id"` - IsAdmin bool `json:"is_admin"` - CanCreateGroup bool `json:"can_create_group"` - CanCreateTeam bool `json:"can_create_team"` - CanCreateProject bool `json:"can_create_project"` -} - -// GetSessionOptions represents the available Session() options. -// -// GitLab API docs: http://doc.gitlab.com/ce/api/session.html#session -type GetSessionOptions struct { - Login string `url:"login,omitempty" json:"login,omitempty"` - Email string `url:"email,omitempty" json:"email,omitempty"` - Password string `url:"password,omitempty" json:"password,omitempty"` -} - -// GetSession logs in to get private token. -// -// GitLab API docs: http://doc.gitlab.com/ce/api/session.html#session -func (s *SessionService) GetSession(opt *GetSessionOptions) (*Session, *Response, error) { - req, err := s.client.NewRequest("POST", "session", opt) - if err != nil { - return nil, nil, err - } - - session := new(Session) - resp, err := s.client.Do(req, session) - if err != nil { - return nil, resp, err - } - - return session, resp, err -} diff --git a/api/settings.go b/api/settings.go index 7eeefa7..fde9910 100644 --- a/api/settings.go +++ b/api/settings.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,34 +21,108 @@ import "time" // SettingsService handles communication with the application SettingsService // related methods of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/settings.html +// GitLab API docs: https://docs.gitlab.com/ce/api/settings.html type SettingsService struct { client *Client } // Settings represents the GitLab application settings. // -// GitLab API docs: http://doc.gitlab.com/ce/api/settings.html +// GitLab API docs: https://docs.gitlab.com/ce/api/settings.html type Settings struct { - ID int `json:"id"` - DefaultProjectsLimit int `json:"default_projects_limit"` - SignupEnabled bool `json:"signup_enabled"` - SigninEnabled bool `json:"signin_enabled"` - GravatarEnabled bool `json:"gravatar_enabled"` - SignInText string `json:"sign_in_text"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - HomePageURL string `json:"home_page_url"` - DefaultBranchProtection int `json:"default_branch_protection"` - TwitterSharingEnabled bool `json:"twitter_sharing_enabled"` - RestrictedVisibilityLevels []VisibilityLevel `json:"restricted_visibility_levels"` - MaxAttachmentSize int `json:"max_attachment_size"` - SessionExpireDelay int `json:"session_expire_delay"` - DefaultProjectVisibility int `json:"default_project_visibility"` - DefaultSnippetVisibility int `json:"default_snippet_visibility"` - RestrictedSignupDomains []string `json:"restricted_signup_domains"` - UserOauthApplications bool `json:"user_oauth_applications"` - AfterSignOutPath string `json:"after_sign_out_path"` + ID int `json:"id"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + AdminNotificationEmail string `json:"admin_notification_email"` + AfterSignOutPath string `json:"after_sign_out_path"` + AfterSignUpText string `json:"after_sign_up_text"` + AkismetAPIKey string `json:"akismet_api_key"` + AkismetEnabled bool `json:"akismet_enabled"` + CircuitbreakerAccessRetries int `json:"circuitbreaker_access_retries"` + CircuitbreakerBackoffThreshold int `json:"circuitbreaker_backoff_threshold"` + CircuitbreakerFailureCountThreshold int `json:"circuitbreaker_failure_count_threshold"` + CircuitbreakerFailureResetTime int `json:"circuitbreaker_failure_reset_time"` + CircuitbreakerFailureWaitTime int `json:"circuitbreaker_failure_wait_time"` + CircuitbreakerStorageTimeout int `json:"circuitbreaker_storage_timeout"` + ClientsideSentryDSN string `json:"clientside_sentry_dsn"` + ClientsideSentryEnabled bool `json:"clientside_sentry_enabled"` + ContainerRegistryTokenExpireDelay int `json:"container_registry_token_expire_delay"` + DefaultArtifactsExpireIn string `json:"default_artifacts_expire_in"` + DefaultBranchProtection int `json:"default_branch_protection"` + DefaultGroupVisibility string `json:"default_group_visibility"` + DefaultProjectVisibility string `json:"default_project_visibility"` + DefaultProjectsLimit int `json:"default_projects_limit"` + DefaultSnippetVisibility string `json:"default_snippet_visibility"` + DisabledOauthSignInSources []string `json:"disabled_oauth_sign_in_sources"` + DomainBlacklistEnabled bool `json:"domain_blacklist_enabled"` + DomainBlacklist []string `json:"domain_blacklist"` + DomainWhitelist []string `json:"domain_whitelist"` + DSAKeyRestriction int `json:"dsa_key_restriction"` + ECDSAKeyRestriction int `json:"ecdsa_key_restriction"` + Ed25519KeyRestriction int `json:"ed25519_key_restriction"` + EmailAuthorInBody bool `json:"email_author_in_body"` + EnabledGitAccessProtocol string `json:"enabled_git_access_protocol"` + GravatarEnabled bool `json:"gravatar_enabled"` + HelpPageHideCommercialContent bool `json:"help_page_hide_commercial_content"` + HelpPageSupportURL string `json:"help_page_support_url"` + HomePageURL string `json:"home_page_url"` + HousekeepingBitmapsEnabled bool `json:"housekeeping_bitmaps_enabled"` + HousekeepingEnabled bool `json:"housekeeping_enabled"` + HousekeepingFullRepackPeriod int `json:"housekeeping_full_repack_period"` + HousekeepingGcPeriod int `json:"housekeeping_gc_period"` + HousekeepingIncrementalRepackPeriod int `json:"housekeeping_incremental_repack_period"` + HTMLEmailsEnabled bool `json:"html_emails_enabled"` + ImportSources []string `json:"import_sources"` + KodingEnabled bool `json:"koding_enabled"` + KodingURL string `json:"koding_url"` + MaxArtifactsSize int `json:"max_artifacts_size"` + MaxAttachmentSize int `json:"max_attachment_size"` + MaxPagesSize int `json:"max_pages_size"` + MetricsEnabled bool `json:"metrics_enabled"` + MetricsHost string `json:"metrics_host"` + MetricsMethodCallThreshold int `json:"metrics_method_call_threshold"` + MetricsPacketSize int `json:"metrics_packet_size"` + MetricsPoolSize int `json:"metrics_pool_size"` + MetricsPort int `json:"metrics_port"` + MetricsSampleInterval int `json:"metrics_sample_interval"` + MetricsTimeout int `json:"metrics_timeout"` + PasswordAuthenticationEnabledForWeb bool `json:"password_authentication_enabled_for_web"` + PasswordAuthenticationEnabledForGit bool `json:"password_authentication_enabled_for_git"` + PerformanceBarAllowedGroupID string `json:"performance_bar_allowed_group_id"` + PerformanceBarEnabled bool `json:"performance_bar_enabled"` + PlantumlEnabled bool `json:"plantuml_enabled"` + PlantumlURL string `json:"plantuml_url"` + PollingIntervalMultiplier float64 `json:"polling_interval_multiplier"` + ProjectExportEnabled bool `json:"project_export_enabled"` + PrometheusMetricsEnabled bool `json:"prometheus_metrics_enabled"` + RecaptchaEnabled bool `json:"recaptcha_enabled"` + RecaptchaPrivateKey string `json:"recaptcha_private_key"` + RecaptchaSiteKey string `json:"recaptcha_site_key"` + RepositoryChecksEnabled bool `json:"repository_checks_enabled"` + RepositoryStorages []string `json:"repository_storages"` + RequireTwoFactorAuthentication bool `json:"require_two_factor_authentication"` + RestrictedVisibilityLevels []VisibilityValue `json:"restricted_visibility_levels"` + RsaKeyRestriction int `json:"rsa_key_restriction"` + SendUserConfirmationEmail bool `json:"send_user_confirmation_email"` + SentryDSN string `json:"sentry_dsn"` + SentryEnabled bool `json:"sentry_enabled"` + SessionExpireDelay int `json:"session_expire_delay"` + SharedRunnersEnabled bool `json:"shared_runners_enabled"` + SharedRunnersText string `json:"shared_runners_text"` + SidekiqThrottlingEnabled bool `json:"sidekiq_throttling_enabled"` + SidekiqThrottlingFactor float64 `json:"sidekiq_throttling_factor"` + SidekiqThrottlingQueues []string `json:"sidekiq_throttling_queues"` + SignInText string `json:"sign_in_text"` + SignupEnabled bool `json:"signup_enabled"` + TerminalMaxSessionTime int `json:"terminal_max_session_time"` + TwoFactorGracePeriod int `json:"two_factor_grace_period"` + UniqueIPsLimitEnabled bool `json:"unique_ips_limit_enabled"` + UniqueIPsLimitPerUser int `json:"unique_ips_limit_per_user"` + UniqueIPsLimitTimeWindow int `json:"unique_ips_limit_time_window"` + UsagePingEnabled bool `json:"usage_ping_enabled"` + UserDefaultExternal bool `json:"user_default_external"` + UserOauthApplications bool `json:"user_oauth_applications"` + VersionCheckEnabled bool `json:"version_check_enabled"` } func (s Settings) String() string { @@ -58,9 +132,9 @@ func (s Settings) String() string { // GetSettings gets the current application settings. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/settings.html#get-current-application.settings -func (s *SettingsService) GetSettings() (*Settings, *Response, error) { - req, err := s.client.NewRequest("GET", "application/settings", nil) +// https://docs.gitlab.com/ce/api/settings.html#get-current-application.settings +func (s *SettingsService) GetSettings(options ...OptionFunc) (*Settings, *Response, error) { + req, err := s.client.NewRequest("GET", "application/settings", nil, options) if err != nil { return nil, nil, err } @@ -77,32 +151,106 @@ func (s *SettingsService) GetSettings() (*Settings, *Response, error) { // UpdateSettingsOptions represents the available UpdateSettings() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/settings.html#change-application.settings +// https://docs.gitlab.com/ce/api/settings.html#change-application.settings type UpdateSettingsOptions struct { - DefaultProjectsLimit int `url:"default_projects_limit,omitempty" json:"default_projects_limit,omitempty"` - SignupEnabled bool `url:"signup_enabled,omitempty" json:"signup_enabled,omitempty"` - SigninEnabled bool `url:"signin_enabled,omitempty" json:"signin_enabled,omitempty"` - GravatarEnabled bool `url:"gravatar_enabled,omitempty" json:"gravatar_enabled,omitempty"` - SignInText string `url:"sign_in_text,omitempty" json:"sign_in_text,omitempty"` - HomePageURL string `url:"home_page_url,omitempty" json:"home_page_url,omitempty"` - DefaultBranchProtection int `url:"default_branch_protection,omitempty" json:"default_branch_protection,omitempty"` - TwitterSharingEnabled bool `url:"twitter_sharing_enabled,omitempty" json:"twitter_sharing_enabled,omitempty"` - RestrictedVisibilityLevels []VisibilityLevel `url:"restricted_visibility_levels,omitempty" json:"restricted_visibility_levels,omitempty"` - MaxAttachmentSize int `url:"max_attachment_size,omitempty" json:"max_attachment_size,omitempty"` - SessionExpireDelay int `url:"session_expire_delay,omitempty" json:"session_expire_delay,omitempty"` - DefaultProjectVisibility int `url:"default_project_visibility,omitempty" json:"default_project_visibility,omitempty"` - DefaultSnippetVisibility int `url:"default_snippet_visibility,omitempty" json:"default_snippet_visibility,omitempty"` - RestrictedSignupDomains []string `url:"restricted_signup_domains,omitempty" json:"restricted_signup_domains,omitempty"` - UserOauthApplications bool `url:"user_oauth_applications,omitempty" json:"user_oauth_applications,omitempty"` - AfterSignOutPath string `url:"after_sign_out_path,omitempty" json:"after_sign_out_path,omitempty"` + AdminNotificationEmail *string `url:"admin_notification_email,omitempty" json:"admin_notification_email,omitempty"` + AfterSignOutPath *string `url:"after_sign_out_path,omitempty" json:"after_sign_out_path,omitempty"` + AfterSignUpText *string `url:"after_sign_up_text,omitempty" json:"after_sign_up_text,omitempty"` + AkismetAPIKey *string `url:"akismet_api_key,omitempty" json:"akismet_api_key,omitempty"` + AkismetEnabled *bool `url:"akismet_enabled,omitempty" json:"akismet_enabled,omitempty"` + CircuitbreakerAccessRetries *int `url:"circuitbreaker_access_retries,omitempty" json:"circuitbreaker_access_retries,omitempty"` + CircuitbreakerBackoffThreshold *int `url:"circuitbreaker_backoff_threshold,omitempty" json:"circuitbreaker_backoff_threshold,omitempty"` + CircuitbreakerFailureCountThreshold *int `url:"circuitbreaker_failure_count_threshold,omitempty" json:"circuitbreaker_failure_count_threshold,omitempty"` + CircuitbreakerFailureResetTime *int `url:"circuitbreaker_failure_reset_time,omitempty" json:"circuitbreaker_failure_reset_time,omitempty"` + CircuitbreakerFailureWaitTime *int `url:"circuitbreaker_failure_wait_time,omitempty" json:"circuitbreaker_failure_wait_time,omitempty"` + CircuitbreakerStorageTimeout *int `url:"circuitbreaker_storage_timeout,omitempty" json:"circuitbreaker_storage_timeout,omitempty"` + ClientsideSentryDSN *string `url:"clientside_sentry_dsn,omitempty" json:"clientside_sentry_dsn,omitempty"` + ClientsideSentryEnabled *bool `url:"clientside_sentry_enabled,omitempty" json:"clientside_sentry_enabled,omitempty"` + ContainerRegistryTokenExpireDelay *int `url:"container_registry_token_expire_delay,omitempty" json:"container_registry_token_expire_delay,omitempty"` + DefaultArtifactsExpireIn *string `url:"default_artifacts_expire_in,omitempty" json:"default_artifacts_expire_in,omitempty"` + DefaultBranchProtection *int `url:"default_branch_protection,omitempty" json:"default_branch_protection,omitempty"` + DefaultGroupVisibility *string `url:"default_group_visibility,omitempty" json:"default_group_visibility,omitempty"` + DefaultProjectVisibility *string `url:"default_project_visibility,omitempty" json:"default_project_visibility,omitempty"` + DefaultProjectsLimit *int `url:"default_projects_limit,omitempty" json:"default_projects_limit,omitempty"` + DefaultSnippetVisibility *string `url:"default_snippet_visibility,omitempty" json:"default_snippet_visibility,omitempty"` + DisabledOauthSignInSources []string `url:"disabled_oauth_sign_in_sources,omitempty" json:"disabled_oauth_sign_in_sources,omitempty"` + DomainBlacklistEnabled *bool `url:"domain_blacklist_enabled,omitempty" json:"domain_blacklist_enabled,omitempty"` + DomainBlacklist []string `url:"domain_blacklist,omitempty" json:"domain_blacklist,omitempty"` + DomainWhitelist []string `url:"domain_whitelist,omitempty" json:"domain_whitelist,omitempty"` + DSAKeyRestriction *int `url:"dsa_key_restriction,omitempty" json:"dsa_key_restriction,omitempty"` + ECDSAKeyRestriction *int `url:"ecdsa_key_restriction,omitempty" json:"ecdsa_key_restriction,omitempty"` + Ed25519KeyRestriction *int `url:"ed25519_key_restriction,omitempty" json:"ed25519_key_restriction,omitempty"` + EmailAuthorInBody *bool `url:"email_author_in_body,omitempty" json:"email_author_in_body,omitempty"` + EnabledGitAccessProtocol *string `url:"enabled_git_access_protocol,omitempty" json:"enabled_git_access_protocol,omitempty"` + GravatarEnabled *bool `url:"gravatar_enabled,omitempty" json:"gravatar_enabled,omitempty"` + HelpPageHideCommercialContent *bool `url:"help_page_hide_commercial_content,omitempty" json:"help_page_hide_commercial_content,omitempty"` + HelpPageSupportURL *string `url:"help_page_support_url,omitempty" json:"help_page_support_url,omitempty"` + HomePageURL *string `url:"home_page_url,omitempty" json:"home_page_url,omitempty"` + HousekeepingBitmapsEnabled *bool `url:"housekeeping_bitmaps_enabled,omitempty" json:"housekeeping_bitmaps_enabled,omitempty"` + HousekeepingEnabled *bool `url:"housekeeping_enabled,omitempty" json:"housekeeping_enabled,omitempty"` + HousekeepingFullRepackPeriod *int `url:"housekeeping_full_repack_period,omitempty" json:"housekeeping_full_repack_period,omitempty"` + HousekeepingGcPeriod *int `url:"housekeeping_gc_period,omitempty" json:"housekeeping_gc_period,omitempty"` + HousekeepingIncrementalRepackPeriod *int `url:"housekeeping_incremental_repack_period,omitempty" json:"housekeeping_incremental_repack_period,omitempty"` + HTMLEmailsEnabled *bool `url:"html_emails_enabled,omitempty" json:"html_emails_enabled,omitempty"` + ImportSources []string `url:"import_sources,omitempty" json:"import_sources,omitempty"` + KodingEnabled *bool `url:"koding_enabled,omitempty" json:"koding_enabled,omitempty"` + KodingURL *string `url:"koding_url,omitempty" json:"koding_url,omitempty"` + MaxArtifactsSize *int `url:"max_artifacts_size,omitempty" json:"max_artifacts_size,omitempty"` + MaxAttachmentSize *int `url:"max_attachment_size,omitempty" json:"max_attachment_size,omitempty"` + MaxPagesSize *int `url:"max_pages_size,omitempty" json:"max_pages_size,omitempty"` + MetricsEnabled *bool `url:"metrics_enabled,omitempty" json:"metrics_enabled,omitempty"` + MetricsHost *string `url:"metrics_host,omitempty" json:"metrics_host,omitempty"` + MetricsMethodCallThreshold *int `url:"metrics_method_call_threshold,omitempty" json:"metrics_method_call_threshold,omitempty"` + MetricsPacketSize *int `url:"metrics_packet_size,omitempty" json:"metrics_packet_size,omitempty"` + MetricsPoolSize *int `url:"metrics_pool_size,omitempty" json:"metrics_pool_size,omitempty"` + MetricsPort *int `url:"metrics_port,omitempty" json:"metrics_port,omitempty"` + MetricsSampleInterval *int `url:"metrics_sample_interval,omitempty" json:"metrics_sample_interval,omitempty"` + MetricsTimeout *int `url:"metrics_timeout,omitempty" json:"metrics_timeout,omitempty"` + PasswordAuthenticationEnabledForWeb *bool `url:"password_authentication_enabled_for_web,omitempty" json:"password_authentication_enabled_for_web,omitempty"` + PasswordAuthenticationEnabledForGit *bool `url:"password_authentication_enabled_for_git,omitempty" json:"password_authentication_enabled_for_git,omitempty"` + PerformanceBarAllowedGroupID *string `url:"performance_bar_allowed_group_id,omitempty" json:"performance_bar_allowed_group_id,omitempty"` + PerformanceBarEnabled *bool `url:"performance_bar_enabled,omitempty" json:"performance_bar_enabled,omitempty"` + PlantumlEnabled *bool `url:"plantuml_enabled,omitempty" json:"plantuml_enabled,omitempty"` + PlantumlURL *string `url:"plantuml_url,omitempty" json:"plantuml_url,omitempty"` + PollingIntervalMultiplier *float64 `url:"polling_interval_multiplier,omitempty" json:"polling_interval_multiplier,omitempty"` + ProjectExportEnabled *bool `url:"project_export_enabled,omitempty" json:"project_export_enabled,omitempty"` + PrometheusMetricsEnabled *bool `url:"prometheus_metrics_enabled,omitempty" json:"prometheus_metrics_enabled,omitempty"` + RecaptchaEnabled *bool `url:"recaptcha_enabled,omitempty" json:"recaptcha_enabled,omitempty"` + RecaptchaPrivateKey *string `url:"recaptcha_private_key,omitempty" json:"recaptcha_private_key,omitempty"` + RecaptchaSiteKey *string `url:"recaptcha_site_key,omitempty" json:"recaptcha_site_key,omitempty"` + RepositoryChecksEnabled *bool `url:"repository_checks_enabled,omitempty" json:"repository_checks_enabled,omitempty"` + RepositoryStorages []string `url:"repository_storages,omitempty" json:"repository_storages,omitempty"` + RequireTwoFactorAuthentication *bool `url:"require_two_factor_authentication,omitempty" json:"require_two_factor_authentication,omitempty"` + RestrictedVisibilityLevels []VisibilityValue `url:"restricted_visibility_levels,omitempty" json:"restricted_visibility_levels,omitempty"` + RsaKeyRestriction *int `url:"rsa_key_restriction,omitempty" json:"rsa_key_restriction,omitempty"` + SendUserConfirmationEmail *bool `url:"send_user_confirmation_email,omitempty" json:"send_user_confirmation_email,omitempty"` + SentryDSN *string `url:"sentry_dsn,omitempty" json:"sentry_dsn,omitempty"` + SentryEnabled *bool `url:"sentry_enabled,omitempty" json:"sentry_enabled,omitempty"` + SessionExpireDelay *int `url:"session_expire_delay,omitempty" json:"session_expire_delay,omitempty"` + SharedRunnersEnabled *bool `url:"shared_runners_enabled,omitempty" json:"shared_runners_enabled,omitempty"` + SharedRunnersText *string `url:"shared_runners_text,omitempty" json:"shared_runners_text,omitempty"` + SidekiqThrottlingEnabled *bool `url:"sidekiq_throttling_enabled,omitempty" json:"sidekiq_throttling_enabled,omitempty"` + SidekiqThrottlingFactor *float64 `url:"sidekiq_throttling_factor,omitempty" json:"sidekiq_throttling_factor,omitempty"` + SidekiqThrottlingQueues []string `url:"sidekiq_throttling_queues,omitempty" json:"sidekiq_throttling_queues,omitempty"` + SignInText *string `url:"sign_in_text,omitempty" json:"sign_in_text,omitempty"` + SignupEnabled *bool `url:"signup_enabled,omitempty" json:"signup_enabled,omitempty"` + TerminalMaxSessionTime *int `url:"terminal_max_session_time,omitempty" json:"terminal_max_session_time,omitempty"` + TwoFactorGracePeriod *int `url:"two_factor_grace_period,omitempty" json:"two_factor_grace_period,omitempty"` + UniqueIPsLimitEnabled *bool `url:"unique_ips_limit_enabled,omitempty" json:"unique_ips_limit_enabled,omitempty"` + UniqueIPsLimitPerUser *int `url:"unique_ips_limit_per_user,omitempty" json:"unique_ips_limit_per_user,omitempty"` + UniqueIPsLimitTimeWindow *int `url:"unique_ips_limit_time_window,omitempty" json:"unique_ips_limit_time_window,omitempty"` + UsagePingEnabled *bool `url:"usage_ping_enabled,omitempty" json:"usage_ping_enabled,omitempty"` + UserDefaultExternal *bool `url:"user_default_external,omitempty" json:"user_default_external,omitempty"` + UserOauthApplications *bool `url:"user_oauth_applications,omitempty" json:"user_oauth_applications,omitempty"` + VersionCheckEnabled *bool `url:"version_check_enabled,omitempty" json:"version_check_enabled,omitempty"` } // UpdateSettings updates the application settings. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/settings.html#change-application.settings -func (s *SettingsService) UpdateSettings(opt *UpdateSettingsOptions) (*Settings, *Response, error) { - req, err := s.client.NewRequest("PUT", "application/settings", opt) +// https://docs.gitlab.com/ce/api/settings.html#change-application.settings +func (s *SettingsService) UpdateSettings(opt *UpdateSettingsOptions, options ...OptionFunc) (*Settings, *Response, error) { + req, err := s.client.NewRequest("PUT", "application/settings", opt, options) if err != nil { return nil, nil, err } diff --git a/api/settings_test.go b/api/settings_test.go new file mode 100644 index 0000000..c3d320e --- /dev/null +++ b/api/settings_test.go @@ -0,0 +1,51 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestGetSettings(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/application/settings", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":1, "default_projects_limit" : 100000}`) + }) + + settings, _, err := client.Settings.GetSettings() + if err != nil { + t.Fatal(err) + } + + want := &Settings{ID: 1, DefaultProjectsLimit: 100000} + if !reflect.DeepEqual(settings, want) { + t.Errorf("Settings.GetSettings returned %+v, want %+v", settings, want) + } +} + +func TestUpdateSettings(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/application/settings", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{"default_projects_limit" : 100}`) + }) + + options := &UpdateSettingsOptions{ + DefaultProjectsLimit: Int(100), + } + settings, _, err := client.Settings.UpdateSettings(options) + if err != nil { + t.Fatal(err) + } + + want := &Settings{DefaultProjectsLimit: 100} + if !reflect.DeepEqual(settings, want) { + t.Errorf("Settings.UpdateSettings returned %+v, want %+v", settings, want) + } +} diff --git a/api/sidekiq_metrics.go b/api/sidekiq_metrics.go new file mode 100644 index 0000000..83e7702 --- /dev/null +++ b/api/sidekiq_metrics.go @@ -0,0 +1,154 @@ +// +// Copyright 2018, Sander van Harmelen +// +// 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 gitlab + +import "time" + +// SidekiqService handles communication with the sidekiq service +// +// GitLab API docs: https://docs.gitlab.com/ce/api/sidekiq_metrics.html +type SidekiqService struct { + client *Client +} + +// QueueMetrics represents the GitLab sidekiq queue metrics. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/sidekiq_metrics.html#get-the-current-queue-metrics +type QueueMetrics struct { + Queues map[string]struct { + Backlog int `json:"backlog"` + Latency int `json:"latency"` + } `json:"queues"` +} + +// GetQueueMetrics lists information about all the registered queues, +// their backlog and their latency. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/sidekiq_metrics.html#get-the-current-queue-metrics +func (s *SidekiqService) GetQueueMetrics(options ...OptionFunc) (*QueueMetrics, *Response, error) { + req, err := s.client.NewRequest("GET", "/sidekiq/queue_metrics", nil, options) + if err != nil { + return nil, nil, err + } + + q := new(QueueMetrics) + resp, err := s.client.Do(req, q) + if err != nil { + return nil, resp, err + } + + return q, resp, err +} + +// ProcessMetrics represents the GitLab sidekiq process metrics. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/sidekiq_metrics.html#get-the-current-process-metrics +type ProcessMetrics struct { + Processes []struct { + Hostname string `json:"hostname"` + Pid int `json:"pid"` + Tag string `json:"tag"` + StartedAt *time.Time `json:"started_at"` + Queues []string `json:"queues"` + Labels []string `json:"labels"` + Concurrency int `json:"concurrency"` + Busy int `json:"busy"` + } `json:"processes"` +} + +// GetProcessMetrics lists information about all the Sidekiq workers registered +// to process your queues. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/sidekiq_metrics.html#get-the-current-process-metrics +func (s *SidekiqService) GetProcessMetrics(options ...OptionFunc) (*ProcessMetrics, *Response, error) { + req, err := s.client.NewRequest("GET", "/sidekiq/process_metrics", nil, options) + if err != nil { + return nil, nil, err + } + + p := new(ProcessMetrics) + resp, err := s.client.Do(req, p) + if err != nil { + return nil, resp, err + } + + return p, resp, err +} + +// JobStats represents the GitLab sidekiq job stats. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/sidekiq_metrics.html#get-the-current-job-statistics +type JobStats struct { + Jobs struct { + Processed int `json:"processed"` + Failed int `json:"failed"` + Enqueued int `json:"enqueued"` + } `json:"jobs"` +} + +// GetJobStats list information about the jobs that Sidekiq has performed. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/sidekiq_metrics.html#get-the-current-job-statistics +func (s *SidekiqService) GetJobStats(options ...OptionFunc) (*JobStats, *Response, error) { + req, err := s.client.NewRequest("GET", "/sidekiq/job_stats", nil, options) + if err != nil { + return nil, nil, err + } + + j := new(JobStats) + resp, err := s.client.Do(req, j) + if err != nil { + return nil, resp, err + } + + return j, resp, err +} + +// CompoundMetrics represents the GitLab sidekiq compounded stats. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/sidekiq_metrics.html#get-a-compound-response-of-all-the-previously-mentioned-metrics +type CompoundMetrics struct { + QueueMetrics + ProcessMetrics + JobStats +} + +// GetCompoundMetrics lists all the currently available information about Sidekiq. +// Get a compound response of all the previously mentioned metrics +// +// GitLab API docs: https://docs.gitlab.com/ce/api/sidekiq_metrics.html#get-the-current-job-statistics +func (s *SidekiqService) GetCompoundMetrics(options ...OptionFunc) (*CompoundMetrics, *Response, error) { + req, err := s.client.NewRequest("GET", "/sidekiq/compound_metrics", nil, options) + if err != nil { + return nil, nil, err + } + + c := new(CompoundMetrics) + resp, err := s.client.Do(req, c) + if err != nil { + return nil, resp, err + } + + return c, resp, err +} diff --git a/api/snippets.go b/api/snippets.go new file mode 100644 index 0000000..be232c8 --- /dev/null +++ b/api/snippets.go @@ -0,0 +1,230 @@ +// +// Copyright 2017, Sander van Harmelen +// +// 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 gitlab + +import ( + "bytes" + "fmt" + "time" +) + +// SnippetsService handles communication with the snippets +// related methods of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/snippets.html +type SnippetsService struct { + client *Client +} + +// Snippet represents a GitLab snippet. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/snippets.html +type Snippet struct { + ID int `json:"id"` + Title string `json:"title"` + FileName string `json:"file_name"` + Description string `json:"description"` + Author struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + } `json:"author"` + UpdatedAt *time.Time `json:"updated_at"` + CreatedAt *time.Time `json:"created_at"` + WebURL string `json:"web_url"` + RawURL string `json:"raw_url"` +} + +func (s Snippet) String() string { + return Stringify(s) +} + +// ListSnippetsOptions represents the available ListSnippets() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/snippets.html#list-snippets +type ListSnippetsOptions ListOptions + +// ListSnippets gets a list of snippets. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/snippets.html#list-snippets +func (s *SnippetsService) ListSnippets(opt *ListSnippetsOptions, options ...OptionFunc) ([]*Snippet, *Response, error) { + req, err := s.client.NewRequest("GET", "snippets", opt, options) + if err != nil { + return nil, nil, err + } + + var ps []*Snippet + resp, err := s.client.Do(req, &ps) + if err != nil { + return nil, resp, err + } + + return ps, resp, err +} + +// GetSnippet gets a single snippet +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/snippets.html#single-snippet +func (s *SnippetsService) GetSnippet(snippet int, options ...OptionFunc) (*Snippet, *Response, error) { + u := fmt.Sprintf("snippets/%d", snippet) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + ps := new(Snippet) + resp, err := s.client.Do(req, ps) + if err != nil { + return nil, resp, err + } + + return ps, resp, err +} + +// CreateSnippetOptions represents the available CreateSnippet() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/snippets.html#create-new-snippet +type CreateSnippetOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + FileName *string `url:"file_name,omitempty" json:"file_name,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + Content *string `url:"content,omitempty" json:"content,omitempty"` + Visibility *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"` +} + +// CreateSnippet creates a new snippet. The user must have permission +// to create new snippets. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/snippets.html#create-new-snippet +func (s *SnippetsService) CreateSnippet(opt *CreateSnippetOptions, options ...OptionFunc) (*Snippet, *Response, error) { + req, err := s.client.NewRequest("POST", "snippets", opt, options) + if err != nil { + return nil, nil, err + } + + ps := new(Snippet) + resp, err := s.client.Do(req, ps) + if err != nil { + return nil, resp, err + } + + return ps, resp, err +} + +// UpdateSnippetOptions represents the available UpdateSnippet() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/snippets.html#update-snippet +type UpdateSnippetOptions struct { + Title *string `url:"title,omitempty" json:"title,omitempty"` + FileName *string `url:"file_name,omitempty" json:"file_name,omitempty"` + Description *string `url:"description,omitempty" json:"description,omitempty"` + Content *string `url:"content,omitempty" json:"content,omitempty"` + Visibility *VisibilityValue `url:"visibility,omitempty" json:"visibility,omitempty"` +} + +// UpdateSnippet updates an existing snippet. The user must have +// permission to change an existing snippet. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/snippets.html#update-snippet +func (s *SnippetsService) UpdateSnippet(snippet int, opt *UpdateSnippetOptions, options ...OptionFunc) (*Snippet, *Response, error) { + u := fmt.Sprintf("snippets/%d", snippet) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + ps := new(Snippet) + resp, err := s.client.Do(req, ps) + if err != nil { + return nil, resp, err + } + + return ps, resp, err +} + +// DeleteSnippet deletes an existing snippet. This is an idempotent +// function and deleting a non-existent snippet still returns a 200 OK status +// code. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/snippets.html#delete-snippet +func (s *SnippetsService) DeleteSnippet(snippet int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("snippets/%d", snippet) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// SnippetContent returns the raw snippet as plain text. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/snippets.html#snippet-content +func (s *SnippetsService) SnippetContent(snippet int, options ...OptionFunc) ([]byte, *Response, error) { + u := fmt.Sprintf("snippets/%d/raw", snippet) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var b bytes.Buffer + resp, err := s.client.Do(req, &b) + if err != nil { + return nil, resp, err + } + + return b.Bytes(), resp, err +} + +// ExploreSnippetsOptions represents the available ExploreSnippets() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/snippets.html#explore-all-public-snippets +type ExploreSnippetsOptions ListOptions + +// ExploreSnippets gets the list of public snippets. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/snippets.html#explore-all-public-snippets +func (s *SnippetsService) ExploreSnippets(opt *ExploreSnippetsOptions, options ...OptionFunc) ([]*Snippet, *Response, error) { + req, err := s.client.NewRequest("GET", "snippets/public", nil, options) + if err != nil { + return nil, nil, err + } + + var ps []*Snippet + resp, err := s.client.Do(req, &ps) + if err != nil { + return nil, resp, err + } + + return ps, resp, err +} diff --git a/api/strings.go b/api/strings.go index e2e8f12..aeefb6b 100644 --- a/api/strings.go +++ b/api/strings.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ package gitlab import ( "bytes" "fmt" - "io" "reflect" ) @@ -35,9 +34,9 @@ func Stringify(message interface{}) string { } // stringifyValue was heavily inspired by the goprotobuf library. -func stringifyValue(w io.Writer, val reflect.Value) { +func stringifyValue(buf *bytes.Buffer, val reflect.Value) { if val.Kind() == reflect.Ptr && val.IsNil() { - w.Write([]byte("")) + buf.WriteString("") return } @@ -45,25 +44,25 @@ func stringifyValue(w io.Writer, val reflect.Value) { switch v.Kind() { case reflect.String: - fmt.Fprintf(w, `"%s"`, v) + fmt.Fprintf(buf, `"%s"`, v) case reflect.Slice: - w.Write([]byte{'['}) + buf.WriteByte('[') for i := 0; i < v.Len(); i++ { if i > 0 { - w.Write([]byte{' '}) + buf.WriteByte(' ') } - stringifyValue(w, v.Index(i)) + stringifyValue(buf, v.Index(i)) } - w.Write([]byte{']'}) + buf.WriteByte(']') return case reflect.Struct: if v.Type().Name() != "" { - w.Write([]byte(v.Type().String())) + buf.WriteString(v.Type().String()) } - w.Write([]byte{'{'}) + buf.WriteByte('{') var sep bool for i := 0; i < v.NumField(); i++ { @@ -76,20 +75,20 @@ func stringifyValue(w io.Writer, val reflect.Value) { } if sep { - w.Write([]byte(", ")) + buf.WriteString(", ") } else { sep = true } - w.Write([]byte(v.Type().Field(i).Name)) - w.Write([]byte{':'}) - stringifyValue(w, fv) + buf.WriteString(v.Type().Field(i).Name) + buf.WriteByte(':') + stringifyValue(buf, fv) } - w.Write([]byte{'}'}) + buf.WriteByte('}') default: if v.CanInterface() { - fmt.Fprint(w, v.Interface()) + fmt.Fprint(buf, v.Interface()) } } } diff --git a/api/system_hooks.go b/api/system_hooks.go index 56add77..d5209d4 100644 --- a/api/system_hooks.go +++ b/api/system_hooks.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,18 +24,18 @@ import ( // SystemHooksService handles communication with the system hooks related // methods of the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/system_hooks.html +// GitLab API docs: https://docs.gitlab.com/ce/api/system_hooks.html type SystemHooksService struct { client *Client } // Hook represents a GitLap system hook. // -// GitLab API docs: http://doc.gitlab.com/ce/api/system_hooks.html +// GitLab API docs: https://docs.gitlab.com/ce/api/system_hooks.html type Hook struct { - ID int `json:"id"` - URL string `json:"url"` - CreatedAt time.Time `json:"created_at"` + ID int `json:"id"` + URL string `json:"url"` + CreatedAt *time.Time `json:"created_at"` } func (h Hook) String() string { @@ -45,9 +45,9 @@ func (h Hook) String() string { // ListHooks gets a list of system hooks. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/system_hooks.html#list-system-hooks -func (s *SystemHooksService) ListHooks() ([]*Hook, *Response, error) { - req, err := s.client.NewRequest("GET", "hooks", nil) +// https://docs.gitlab.com/ce/api/system_hooks.html#list-system-hooks +func (s *SystemHooksService) ListHooks(options ...OptionFunc) ([]*Hook, *Response, error) { + req, err := s.client.NewRequest("GET", "hooks", nil, options) if err != nil { return nil, nil, err } @@ -64,17 +64,17 @@ func (s *SystemHooksService) ListHooks() ([]*Hook, *Response, error) { // AddHookOptions represents the available AddHook() options. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/system_hooks.html#add-new-system-hook-hook +// https://docs.gitlab.com/ce/api/system_hooks.html#add-new-system-hook-hook type AddHookOptions struct { - URL string `url:"url,omitempty" json:"url,omitempty"` + URL *string `url:"url,omitempty" json:"url,omitempty"` } // AddHook adds a new system hook hook. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/system_hooks.html#add-new-system-hook-hook -func (s *SystemHooksService) AddHook(opt *AddHookOptions) (*Hook, *Response, error) { - req, err := s.client.NewRequest("POST", "hooks", opt) +// https://docs.gitlab.com/ce/api/system_hooks.html#add-new-system-hook-hook +func (s *SystemHooksService) AddHook(opt *AddHookOptions, options ...OptionFunc) (*Hook, *Response, error) { + req, err := s.client.NewRequest("POST", "hooks", opt, options) if err != nil { return nil, nil, err } @@ -88,9 +88,9 @@ func (s *SystemHooksService) AddHook(opt *AddHookOptions) (*Hook, *Response, err return h, resp, err } -// HookEvent represents an event triggert by a GitLab system hook. +// HookEvent represents an event trigger by a GitLab system hook. // -// GitLab API docs: http://doc.gitlab.com/ce/api/system_hooks.html +// GitLab API docs: https://docs.gitlab.com/ce/api/system_hooks.html type HookEvent struct { EventName string `json:"event_name"` Name string `json:"name"` @@ -107,11 +107,11 @@ func (h HookEvent) String() string { // TestHook tests a system hook. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/system_hooks.html#test-system-hook -func (s *SystemHooksService) TestHook(hook int) (*HookEvent, *Response, error) { +// https://docs.gitlab.com/ce/api/system_hooks.html#test-system-hook +func (s *SystemHooksService) TestHook(hook int, options ...OptionFunc) (*HookEvent, *Response, error) { u := fmt.Sprintf("hooks/%d", hook) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -130,19 +130,14 @@ func (s *SystemHooksService) TestHook(hook int) (*HookEvent, *Response, error) { // is also returned as JSON. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/system_hooks.html#delete-system-hook -func (s *SystemHooksService) DeleteHook(hook int) (*Response, error) { +// https://docs.gitlab.com/ce/api/system_hooks.html#delete-system-hook +func (s *SystemHooksService) DeleteHook(hook int, options ...OptionFunc) (*Response, error) { u := fmt.Sprintf("hooks/%d", hook) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) - if err != nil { - return resp, err - } - - return resp, err + return s.client.Do(req, nil) } diff --git a/api/tags.go b/api/tags.go new file mode 100644 index 0000000..4e8a713 --- /dev/null +++ b/api/tags.go @@ -0,0 +1,236 @@ +// +// Copyright 2017, Sander van Harmelen +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// TagsService handles communication with the tags related methods +// of the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/tags.html +type TagsService struct { + client *Client +} + +// Tag represents a GitLab tag. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/tags.html +type Tag struct { + Commit *Commit `json:"commit"` + Release *Release `json:"release"` + Name string `json:"name"` + Message string `json:"message"` +} + +// Release represents a GitLab version release. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/tags.html +type Release struct { + TagName string `json:"tag_name"` + Description string `json:"description"` +} + +func (t Tag) String() string { + return Stringify(t) +} + +// ListTagsOptions represents the available ListTags() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#list-project-repository-tags +type ListTagsOptions struct { + ListOptions + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` +} + +// ListTags gets a list of tags from a project, sorted by name in reverse +// alphabetical order. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#list-project-repository-tags +func (s *TagsService) ListTags(pid interface{}, opt *ListTagsOptions, options ...OptionFunc) ([]*Tag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/tags", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var t []*Tag + resp, err := s.client.Do(req, &t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// GetTag a specific repository tag determined by its name. It returns 200 together +// with the tag information if the tag exists. It returns 404 if the tag does not exist. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#get-a-single-repository-tag +func (s *TagsService) GetTag(pid interface{}, tag string, options ...OptionFunc) (*Tag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/tags/%s", url.QueryEscape(project), url.QueryEscape(tag)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var t *Tag + resp, err := s.client.Do(req, &t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// CreateTagOptions represents the available CreateTag() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#create-a-new-tag +type CreateTagOptions struct { + TagName *string `url:"tag_name,omitempty" json:"tag_name,omitempty"` + Ref *string `url:"ref,omitempty" json:"ref,omitempty"` + Message *string `url:"message,omitempty" json:"message,omitempty"` + ReleaseDescription *string `url:"release_description:omitempty" json:"release_description,omitempty"` +} + +// CreateTag creates a new tag in the repository that points to the supplied ref. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#create-a-new-tag +func (s *TagsService) CreateTag(pid interface{}, opt *CreateTagOptions, options ...OptionFunc) (*Tag, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/tags", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + t := new(Tag) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// DeleteTag deletes a tag of a repository with given name. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#delete-a-tag +func (s *TagsService) DeleteTag(pid interface{}, tag string, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/repository/tags/%s", url.QueryEscape(project), url.QueryEscape(tag)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// CreateReleaseOptions represents the available CreateRelease() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#create-a-new-release +type CreateReleaseOptions struct { + Description *string `url:"description:omitempty" json:"description,omitempty"` +} + +// CreateRelease Add release notes to the existing git tag. +// If there already exists a release for the given tag, status code 409 is returned. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#create-a-new-release +func (s *TagsService) CreateRelease(pid interface{}, tag string, opt *CreateReleaseOptions, options ...OptionFunc) (*Release, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/tags/%s/release", url.QueryEscape(project), url.QueryEscape(tag)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + r := new(Release) + resp, err := s.client.Do(req, r) + if err != nil { + return nil, resp, err + } + + return r, resp, err +} + +// UpdateReleaseOptions represents the available UpdateRelease() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#update-a-release +type UpdateReleaseOptions struct { + Description *string `url:"description:omitempty" json:"description,omitempty"` +} + +// UpdateRelease Updates the release notes of a given release. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/tags.html#update-a-release +func (s *TagsService) UpdateRelease(pid interface{}, tag string, opt *UpdateReleaseOptions, options ...OptionFunc) (*Release, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/repository/tags/%s/release", url.QueryEscape(project), url.QueryEscape(tag)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + r := new(Release) + resp, err := s.client.Do(req, r) + if err != nil { + return nil, resp, err + } + + return r, resp, err +} diff --git a/api/tags_test.go b/api/tags_test.go new file mode 100644 index 0000000..7492a52 --- /dev/null +++ b/api/tags_test.go @@ -0,0 +1,74 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestListTags(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/repository/tags", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"name": "1.0.0"},{"name": "1.0.1"}]`) + }) + + opt := &ListTagsOptions{ListOptions: ListOptions{Page: 2, PerPage: 3}} + + tags, _, err := client.Tags.ListTags(1, opt) + if err != nil { + t.Errorf("Tags.ListTags returned error: %v", err) + } + + want := []*Tag{{Name: "1.0.0"}, {Name: "1.0.1"}} + if !reflect.DeepEqual(want, tags) { + t.Errorf("Tags.ListTags returned %+v, want %+v", tags, want) + } +} + +func TestCreateRelease(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/repository/tags/1.0.0/release", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"tag_name": "1.0.0", "description": "Amazing release. Wow"}`) + }) + + opt := &CreateReleaseOptions{Description: String("Amazing release. Wow")} + + release, _, err := client.Tags.CreateRelease(1, "1.0.0", opt) + if err != nil { + t.Errorf("Tags.CreateRelease returned error: %v", err) + } + + want := &Release{TagName: "1.0.0", Description: "Amazing release. Wow"} + if !reflect.DeepEqual(want, release) { + t.Errorf("Tags.CreateRelease returned %+v, want %+v", release, want) + } +} + +func TestUpdateRelease(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/projects/1/repository/tags/1.0.0/release", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{"tag_name": "1.0.0", "description": "Amazing release. Wow!"}`) + }) + + opt := &UpdateReleaseOptions{Description: String("Amazing release. Wow!")} + + release, _, err := client.Tags.UpdateRelease(1, "1.0.0", opt) + if err != nil { + t.Errorf("Tags.UpdateRelease returned error: %v", err) + } + + want := &Release{TagName: "1.0.0", Description: "Amazing release. Wow!"} + if !reflect.DeepEqual(want, release) { + t.Errorf("Tags.UpdateRelease returned %+v, want %+v", release, want) + } +} diff --git a/api/time_stats.go b/api/time_stats.go new file mode 100644 index 0000000..5e3bff9 --- /dev/null +++ b/api/time_stats.go @@ -0,0 +1,163 @@ +package gitlab + +import ( + "fmt" + "net/url" +) + +// timeStatsService handles communication with the time tracking related +// methods of the GitLab API. +// +// GitLab docs: https://docs.gitlab.com/ce/workflow/time_tracking.html +type timeStatsService struct { + client *Client +} + +// TimeStats represents the time estimates and time spent for an issue. +// +// GitLab docs: https://docs.gitlab.com/ce/workflow/time_tracking.html +type TimeStats struct { + HumanTimeEstimate string `json:"human_time_estimate"` + HumanTotalTimeSpent string `json:"human_total_time_spent"` + TimeEstimate int `json:"time_estimate"` + TotalTimeSpent int `json:"total_time_spent"` +} + +func (t TimeStats) String() string { + return Stringify(t) +} + +// SetTimeEstimateOptions represents the available SetTimeEstimate() +// options. +// +// GitLab docs: https://docs.gitlab.com/ce/workflow/time_tracking.html +type SetTimeEstimateOptions struct { + Duration *string `url:"duration,omitempty" json:"duration,omitempty"` +} + +// setTimeEstimate sets the time estimate for a single project issue. +// +// GitLab docs: https://docs.gitlab.com/ce/workflow/time_tracking.html +func (s *timeStatsService) setTimeEstimate(pid interface{}, entity string, issue int, opt *SetTimeEstimateOptions, options ...OptionFunc) (*TimeStats, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/time_estimate", url.QueryEscape(project), entity, issue) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + t := new(TimeStats) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// resetTimeEstimate resets the time estimate for a single project issue. +// +// GitLab docs: https://docs.gitlab.com/ce/workflow/time_tracking.html +func (s *timeStatsService) resetTimeEstimate(pid interface{}, entity string, issue int, options ...OptionFunc) (*TimeStats, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/reset_time_estimate", url.QueryEscape(project), entity, issue) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + t := new(TimeStats) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// AddSpentTimeOptions represents the available AddSpentTime() options. +// +// GitLab docs: https://docs.gitlab.com/ce/workflow/time_tracking.html +type AddSpentTimeOptions struct { + Duration *string `url:"duration,omitempty" json:"duration,omitempty"` +} + +// addSpentTime adds spent time for a single project issue. +// +// GitLab docs: https://docs.gitlab.com/ce/workflow/time_tracking.html +func (s *timeStatsService) addSpentTime(pid interface{}, entity string, issue int, opt *AddSpentTimeOptions, options ...OptionFunc) (*TimeStats, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/add_spent_time", url.QueryEscape(project), entity, issue) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + t := new(TimeStats) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// resetSpentTime resets the spent time for a single project issue. +// +// GitLab docs: https://docs.gitlab.com/ce/workflow/time_tracking.html +func (s *timeStatsService) resetSpentTime(pid interface{}, entity string, issue int, options ...OptionFunc) (*TimeStats, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/reset_spent_time", url.QueryEscape(project), entity, issue) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, nil, err + } + + t := new(TimeStats) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// getTimeSpent gets the spent time for a single project issue. +// +// GitLab docs: https://docs.gitlab.com/ce/workflow/time_tracking.html +func (s *timeStatsService) getTimeSpent(pid interface{}, entity string, issue int, options ...OptionFunc) (*TimeStats, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/%s/%d/time_stats", url.QueryEscape(project), entity, issue) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + t := new(TimeStats) + resp, err := s.client.Do(req, t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} diff --git a/api/todos.go b/api/todos.go new file mode 100644 index 0000000..812181d --- /dev/null +++ b/api/todos.go @@ -0,0 +1,175 @@ +package gitlab + +import "time" +import "fmt" + +// TodosService handles communication with the todos related methods of +// the Gitlab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/todos.html +type TodosService struct { + client *Client +} + +// TodoAction represents the available actions that can be performed on a todo. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/todos.html +type TodoAction string + +// The available todo actions. +const ( + TodoAssigned TodoAction = "assigned" + TodoMentioned TodoAction = "mentioned" + TodoBuildFailed TodoAction = "build_failed" + TodoMarked TodoAction = "marked" + TodoApprovalRequired TodoAction = "approval_required" + TodoDirectlyAddressed TodoAction = "directly_addressed" +) + +// TodoTarget represents a todo target of type Issue or MergeRequest +type TodoTarget struct { + // TODO: replace both Assignee and Author structs with v4 User struct + Assignee struct { + Name string `json:"name"` + Username string `json:"username"` + ID int `json:"id"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + } `json:"assignee"` + Author struct { + Name string `json:"name"` + Username string `json:"username"` + ID int `json:"id"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + } `json:"author"` + CreatedAt *time.Time `json:"created_at"` + Description string `json:"description"` + Downvotes int `json:"downvotes"` + ID int `json:"id"` + IID int `json:"iid"` + Labels []string `json:"labels"` + Milestone Milestone `json:"milestone"` + ProjectID int `json:"project_id"` + State string `json:"state"` + Subscribed bool `json:"subscribed"` + Title string `json:"title"` + UpdatedAt *time.Time `json:"updated_at"` + Upvotes int `json:"upvotes"` + UserNotesCount int `json:"user_notes_count"` + WebURL string `json:"web_url"` + + // Only available for type Issue + Confidential bool `json:"confidential"` + DueDate string `json:"due_date"` + Weight int `json:"weight"` + + // Only available for type MergeRequest + ApprovalsBeforeMerge int `json:"approvals_before_merge"` + ForceRemoveSourceBranch bool `json:"force_remove_source_branch"` + MergeCommitSHA string `json:"merge_commit_sha"` + MergeWhenPipelineSucceeds bool `json:"merge_when_pipeline_succeeds"` + MergeStatus string `json:"merge_status"` + SHA string `json:"sha"` + ShouldRemoveSourceBranch bool `json:"should_remove_source_branch"` + SourceBranch string `json:"source_branch"` + SourceProjectID int `json:"source_project_id"` + Squash bool `json:"squash"` + TargetBranch string `json:"target_branch"` + TargetProjectID int `json:"target_project_id"` + WorkInProgress bool `json:"work_in_progress"` +} + +// Todo represents a GitLab todo. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/todos.html +type Todo struct { + ID int `json:"id"` + Project struct { + ID int `json:"id"` + HTTPURLToRepo string `json:"http_url_to_repo"` + WebURL string `json:"web_url"` + Name string `json:"name"` + NameWithNamespace string `json:"name_with_namespace"` + Path string `json:"path"` + PathWithNamespace string `json:"path_with_namespace"` + } `json:"project"` + Author struct { + ID int `json:"id"` + Name string `json:"name"` + Username string `json:"username"` + State string `json:"state"` + AvatarURL string `json:"avatar_url"` + WebURL string `json:"web_url"` + } `json:"author"` + ActionName TodoAction `json:"action_name"` + TargetType string `json:"target_type"` + Target TodoTarget `json:"target"` + TargetURL string `json:"target_url"` + Body string `json:"body"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` +} + +func (t Todo) String() string { + return Stringify(t) +} + +// ListTodosOptions represents the available ListTodos() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/todos.html#get-a-list-of-todos +type ListTodosOptions struct { + Action *TodoAction `url:"action,omitempty" json:"action,omitempty"` + AuthorID *int `url:"author_id,omitempty" json:"author_id,omitempty"` + ProjectID *int `url:"project_id,omitempty" json:"project_id,omitempty"` + State *string `url:"state,omitempty" json:"state,omitempty"` + Type *string `url:"type,omitempty" json:"type,omitempty"` +} + +// ListTodos lists all todos created by authenticated user. +// When no filter is applied, it returns all pending todos for the current user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/todos.html#get-a-list-of-todos +func (s *TodosService) ListTodos(opt *ListTodosOptions, options ...OptionFunc) ([]*Todo, *Response, error) { + req, err := s.client.NewRequest("GET", "todos", opt, options) + if err != nil { + return nil, nil, err + } + + var t []*Todo + resp, err := s.client.Do(req, &t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// MarkTodoAsDone marks a single pending todo given by its ID for the current user as done. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/todos.html#mark-a-todo-as-done +func (s *TodosService) MarkTodoAsDone(id int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("todos/%d/mark_as_done", id) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// MarkAllTodosAsDone marks all pending todos for the current user as done. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/todos.html#mark-all-todos-as-done +func (s *TodosService) MarkAllTodosAsDone(options ...OptionFunc) (*Response, error) { + req, err := s.client.NewRequest("POST", "todos/mark_as_done", nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/api/todos_test.go b/api/todos_test.go new file mode 100644 index 0000000..2068f51 --- /dev/null +++ b/api/todos_test.go @@ -0,0 +1,62 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestListTodos(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/todos", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1,"state": "pending","target":{"id":1,"approvals_before_merge":2}},{"id":2,"state":"pending","target":{"id":2,"approvals_before_merge":null}}]`) + }) + + opts := &ListTodosOptions{} + todos, _, err := client.Todos.ListTodos(opts) + + if err != nil { + t.Errorf("Todos.ListTodos returned error: %v", err) + } + + want := []*Todo{{ID: 1, State: "pending", Target: TodoTarget{ID: 1, ApprovalsBeforeMerge: 2}}, {ID: 2, State: "pending", Target: TodoTarget{ID: 2}}} + if !reflect.DeepEqual(want, todos) { + t.Errorf("Todos.ListTodos returned %+v, want %+v", todos, want) + } + +} + +func TestMarkAllTodosAsDone(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/todos/mark_as_done", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusNoContent) + }) + + _, err := client.Todos.MarkAllTodosAsDone() + + if err != nil { + t.Fatalf("Todos.MarkTodosRead returns an error: %v", err) + } +} + +func TestMarkTodoAsDone(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/todos/1/mark_as_done", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + }) + + _, err := client.Todos.MarkTodoAsDone(1) + + if err != nil { + t.Fatalf("Todos.MarkTodoRead returns an error: %v", err) + } +} diff --git a/api/users.go b/api/users.go index 8ead30b..6a2f715 100644 --- a/api/users.go +++ b/api/users.go @@ -1,5 +1,5 @@ // -// Copyright 2015, Sander van Harmelen +// Copyright 2017, Sander van Harmelen // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package gitlab import ( + "errors" "fmt" "time" ) @@ -24,53 +25,81 @@ import ( // UsersService handles communication with the user related methods of // the GitLab API. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html type UsersService struct { client *Client } // User represents a GitLab user. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html +// GitLab API docs: https://docs.gitlab.com/ee/api/users.html type User struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Name string `json:"name"` - State string `json:"state"` - CreatedAt time.Time `json:"created_at"` - Bio string `json:"bio"` - Skype string `json:"skype"` - Linkedin string `json:"linkedin"` - Twitter string `json:"twitter"` - WebsiteURL string `json:"website_url"` - ExternUID string `json:"extern_uid"` - Provider string `json:"provider"` - ThemeID int `json:"theme_id"` - ColorSchemeID int `json:"color_scheme_id"` - IsAdmin bool `json:"is_admin"` - AvatarURL string `json:"avatar_url"` - CanCreateGroup bool `json:"can_create_group"` - CanCreateProject bool `json:"can_create_project"` - ProjectsLimit int `json:"projects_limit"` - CurrentSignInAt *time.Time `json:"current_sign_in_at"` - TwoFactorEnabled bool `json:"two_factor_enabled"` + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Name string `json:"name"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` + Bio string `json:"bio"` + Location string `json:"location"` + PublicEmail string `json:"public_email"` + Skype string `json:"skype"` + Linkedin string `json:"linkedin"` + Twitter string `json:"twitter"` + WebsiteURL string `json:"website_url"` + Organization string `json:"organization"` + ExternUID string `json:"extern_uid"` + Provider string `json:"provider"` + ThemeID int `json:"theme_id"` + LastActivityOn *ISOTime `json:"last_activity_on"` + ColorSchemeID int `json:"color_scheme_id"` + IsAdmin bool `json:"is_admin"` + AvatarURL string `json:"avatar_url"` + CanCreateGroup bool `json:"can_create_group"` + CanCreateProject bool `json:"can_create_project"` + ProjectsLimit int `json:"projects_limit"` + CurrentSignInAt *time.Time `json:"current_sign_in_at"` + LastSignInAt *time.Time `json:"last_sign_in_at"` + ConfirmedAt *time.Time `json:"confirmed_at"` + TwoFactorEnabled bool `json:"two_factor_enabled"` + Identities []*UserIdentity `json:"identities"` + External bool `json:"external"` + PrivateProfile bool `json:"private_profile"` + SharedRunnersMinutesLimit int `json:"shared_runners_minutes_limit"` + CustomAttributes []*CustomAttribute `json:"custom_attributes"` +} + +// UserIdentity represents a user identity. +type UserIdentity struct { + Provider string `json:"provider"` + ExternUID string `json:"extern_uid"` } // ListUsersOptions represents the available ListUsers() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#list-users +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-users type ListUsersOptions struct { ListOptions - Active bool `url:"active,omitempty" json:"active,omitempty"` - Search string `url:"search,omitempty" json:"search,omitempty"` + Active *bool `url:"active,omitempty" json:"active,omitempty"` + Blocked *bool `url:"blocked,omitempty" json:"blocked,omitempty"` + + // The options below are only available for admins. + Search *string `url:"search,omitempty" json:"search,omitempty"` + Username *string `url:"username,omitempty" json:"username,omitempty"` + ExternalUID *string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"` + Provider *string `url:"provider,omitempty" json:"provider,omitempty"` + CreatedBefore *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"` + CreatedAfter *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"` + OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"` + Sort *string `url:"sort,omitempty" json:"sort,omitempty"` + WithCustomAttributes *bool `url:"with_custom_attributes,omitempty" json:"with_custom_attributes,omitempty"` } // ListUsers gets a list of users. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#list-users -func (s *UsersService) ListUsers(opt *ListUsersOptions) ([]*User, *Response, error) { - req, err := s.client.NewRequest("GET", "users", opt) +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-users +func (s *UsersService) ListUsers(opt *ListUsersOptions, options ...OptionFunc) ([]*User, *Response, error) { + req, err := s.client.NewRequest("GET", "users", opt, options) if err != nil { return nil, nil, err } @@ -86,11 +115,11 @@ func (s *UsersService) ListUsers(opt *ListUsersOptions) ([]*User, *Response, err // GetUser gets a single user. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#single-user -func (s *UsersService) GetUser(user int) (*User, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#single-user +func (s *UsersService) GetUser(user int, options ...OptionFunc) (*User, *Response, error) { u := fmt.Sprintf("users/%d", user) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -106,30 +135,34 @@ func (s *UsersService) GetUser(user int) (*User, *Response, error) { // CreateUserOptions represents the available CreateUser() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#user-creation +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-creation type CreateUserOptions struct { - Email string `url:"email,omitempty" json:"email,omitempty"` - Password string `url:"password,omitempty" json:"password,omitempty"` - Username string `url:"username,omitempty" json:"username,omitempty"` - Name string `url:"name,omitempty" json:"name,omitempty"` - Skype string `url:"skype,omitempty" json:"skype,omitempty"` - Linkedin string `url:"linkedin,omitempty" json:"linkedin,omitempty"` - Twitter string `url:"twitter,omitempty" json:"twitter,omitempty"` - WebsiteURL string `url:"website_url,omitempty" json:"website_url,omitempty"` - ProjectsLimit int `url:"projects_limit,omitempty" json:"projects_limit,omitempty"` - ExternUID string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"` - Provider string `url:"provider,omitempty" json:"provider,omitempty"` - Bio string `url:"bio,omitempty" json:"bio,omitempty"` - Admin bool `url:"admin,omitempty" json:"admin,omitempty"` - CanCreateGroup bool `url:"can_create_group,omitempty" json:"can_create_group,omitempty"` - Confirm bool `url:"confirm,omitempty" json:"confirm,omitempty"` + Email *string `url:"email,omitempty" json:"email,omitempty"` + Password *string `url:"password,omitempty" json:"password,omitempty"` + ResetPassword *bool `url:"reset_password,omitempty" json:"reset_password,omitempty"` + Username *string `url:"username,omitempty" json:"username,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + Skype *string `url:"skype,omitempty" json:"skype,omitempty"` + Linkedin *string `url:"linkedin,omitempty" json:"linkedin,omitempty"` + Twitter *string `url:"twitter,omitempty" json:"twitter,omitempty"` + WebsiteURL *string `url:"website_url,omitempty" json:"website_url,omitempty"` + Organization *string `url:"organization,omitempty" json:"organization,omitempty"` + ProjectsLimit *int `url:"projects_limit,omitempty" json:"projects_limit,omitempty"` + ExternUID *string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"` + Provider *string `url:"provider,omitempty" json:"provider,omitempty"` + Bio *string `url:"bio,omitempty" json:"bio,omitempty"` + Location *string `url:"location,omitempty" json:"location,omitempty"` + Admin *bool `url:"admin,omitempty" json:"admin,omitempty"` + CanCreateGroup *bool `url:"can_create_group,omitempty" json:"can_create_group,omitempty"` + SkipConfirmation *bool `url:"skip_confirmation,omitempty" json:"skip_confirmation,omitempty"` + External *bool `url:"external,omitempty" json:"external,omitempty"` } // CreateUser creates a new user. Note only administrators can create new users. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#user-creation -func (s *UsersService) CreateUser(opt *CreateUserOptions) (*User, *Response, error) { - req, err := s.client.NewRequest("POST", "users", opt) +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-creation +func (s *UsersService) CreateUser(opt *CreateUserOptions, options ...OptionFunc) (*User, *Response, error) { + req, err := s.client.NewRequest("POST", "users", opt, options) if err != nil { return nil, nil, err } @@ -145,32 +178,36 @@ func (s *UsersService) CreateUser(opt *CreateUserOptions) (*User, *Response, err // ModifyUserOptions represents the available ModifyUser() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#user-modification +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-modification type ModifyUserOptions struct { - Email string `url:"email,omitempty" json:"email,omitempty"` - Password string `url:"password,omitempty" json:"password,omitempty"` - Username string `url:"username,omitempty" json:"username,omitempty"` - Name string `url:"name,omitempty" json:"name,omitempty"` - Skype string `url:"skype,omitempty" json:"skype,omitempty"` - Linkedin string `url:"linkedin,omitempty" json:"linkedin,omitempty"` - Twitter string `url:"twitter,omitempty" json:"twitter,omitempty"` - WebsiteURL string `url:"website_url,omitempty" json:"website_url,omitempty"` - ProjectsLimit int `url:"projects_limit,omitempty" json:"projects_limit,omitempty"` - ExternUID string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"` - Provider string `url:"provider,omitempty" json:"provider,omitempty"` - Bio string `url:"bio,omitempty" json:"bio,omitempty"` - Admin bool `url:"admin,omitempty" json:"admin,omitempty"` - CanCreateGroup bool `url:"can_create_group,omitempty" json:"can_create_group,omitempty"` + Email *string `url:"email,omitempty" json:"email,omitempty"` + Password *string `url:"password,omitempty" json:"password,omitempty"` + Username *string `url:"username,omitempty" json:"username,omitempty"` + Name *string `url:"name,omitempty" json:"name,omitempty"` + Skype *string `url:"skype,omitempty" json:"skype,omitempty"` + Linkedin *string `url:"linkedin,omitempty" json:"linkedin,omitempty"` + Twitter *string `url:"twitter,omitempty" json:"twitter,omitempty"` + WebsiteURL *string `url:"website_url,omitempty" json:"website_url,omitempty"` + Organization *string `url:"organization,omitempty" json:"organization,omitempty"` + ProjectsLimit *int `url:"projects_limit,omitempty" json:"projects_limit,omitempty"` + ExternUID *string `url:"extern_uid,omitempty" json:"extern_uid,omitempty"` + Provider *string `url:"provider,omitempty" json:"provider,omitempty"` + Bio *string `url:"bio,omitempty" json:"bio,omitempty"` + Location *string `url:"location,omitempty" json:"location,omitempty"` + Admin *bool `url:"admin,omitempty" json:"admin,omitempty"` + CanCreateGroup *bool `url:"can_create_group,omitempty" json:"can_create_group,omitempty"` + SkipReconfirmation *bool `url:"skip_reconfirmation,omitempty" json:"skip_reconfirmation,omitempty"` + External *bool `url:"external,omitempty" json:"external,omitempty"` } // ModifyUser modifies an existing user. Only administrators can change attributes // of a user. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#user-modification -func (s *UsersService) ModifyUser(user int, opt *ModifyUserOptions) (*User, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-modification +func (s *UsersService) ModifyUser(user int, opt *ModifyUserOptions, options ...OptionFunc) (*User, *Response, error) { u := fmt.Sprintf("users/%d", user) - req, err := s.client.NewRequest("PUT", u, opt) + req, err := s.client.NewRequest("PUT", u, opt, options) if err != nil { return nil, nil, err } @@ -190,28 +227,23 @@ func (s *UsersService) ModifyUser(user int, opt *ModifyUserOptions) (*User, *Res // actually deleted or not. In the former the user is returned and in the // latter not. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#user-deletion -func (s *UsersService) DeleteUser(user int) (*Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#user-deletion +func (s *UsersService) DeleteUser(user int, options ...OptionFunc) (*Response, error) { u := fmt.Sprintf("users/%d", user) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) - if err != nil { - return resp, err - } - - return resp, err + return s.client.Do(req, nil) } // CurrentUser gets currently authenticated user. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#current-user -func (s *UsersService) CurrentUser() (*User, *Response, error) { - req, err := s.client.NewRequest("GET", "user", nil) +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#current-user +func (s *UsersService) CurrentUser(options ...OptionFunc) (*User, *Response, error) { + req, err := s.client.NewRequest("GET", "user", nil, options) if err != nil { return nil, nil, err } @@ -227,19 +259,19 @@ func (s *UsersService) CurrentUser() (*User, *Response, error) { // SSHKey represents a SSH key. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#list-ssh-keys +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-ssh-keys type SSHKey struct { - ID int `json:"id"` - Title string `json:"title"` - Key string `json:"key"` - CreatedAt time.Time `json:"created_at"` + ID int `json:"id"` + Title string `json:"title"` + Key string `json:"key"` + CreatedAt *time.Time `json:"created_at"` } // ListSSHKeys gets a list of currently authenticated user's SSH keys. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#list-ssh-keys -func (s *UsersService) ListSSHKeys() ([]*SSHKey, *Response, error) { - req, err := s.client.NewRequest("GET", "user/keys", nil) +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-ssh-keys +func (s *UsersService) ListSSHKeys(options ...OptionFunc) ([]*SSHKey, *Response, error) { + req, err := s.client.NewRequest("GET", "user/keys", nil, options) if err != nil { return nil, nil, err } @@ -253,15 +285,21 @@ func (s *UsersService) ListSSHKeys() ([]*SSHKey, *Response, error) { return k, resp, err } +// ListSSHKeysForUserOptions represents the available ListSSHKeysForUser() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#list-ssh-keys-for-user +type ListSSHKeysForUserOptions ListOptions + // ListSSHKeysForUser gets a list of a specified user's SSH keys. Available // only for admin // // GitLab API docs: -// http://doc.gitlab.com/ce/api/users.html#list-ssh-keys-for-user -func (s *UsersService) ListSSHKeysForUser(user int) ([]*SSHKey, *Response, error) { +// https://docs.gitlab.com/ce/api/users.html#list-ssh-keys-for-user +func (s *UsersService) ListSSHKeysForUser(user int, opt *ListSSHKeysForUserOptions, options ...OptionFunc) ([]*SSHKey, *Response, error) { u := fmt.Sprintf("users/%d/keys", user) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { return nil, nil, err } @@ -277,11 +315,11 @@ func (s *UsersService) ListSSHKeysForUser(user int) ([]*SSHKey, *Response, error // GetSSHKey gets a single key. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#single-ssh-key -func (s *UsersService) GetSSHKey(kid int) (*SSHKey, *Response, error) { - u := fmt.Sprintf("user/keys/%d", kid) +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#single-ssh-key +func (s *UsersService) GetSSHKey(key int, options ...OptionFunc) (*SSHKey, *Response, error) { + u := fmt.Sprintf("user/keys/%d", key) - req, err := s.client.NewRequest("GET", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } @@ -297,17 +335,17 @@ func (s *UsersService) GetSSHKey(kid int) (*SSHKey, *Response, error) { // AddSSHKeyOptions represents the available AddSSHKey() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/projects.html#add-ssh-key +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#add-ssh-key type AddSSHKeyOptions struct { - Title string `url:"title,omitempty" json:"title,omitempty"` - Key string `url:"key,omitempty" json:"key,omitempty"` + Title *string `url:"title,omitempty" json:"title,omitempty"` + Key *string `url:"key,omitempty" json:"key,omitempty"` } // AddSSHKey creates a new key owned by the currently authenticated user. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#add-ssh-key -func (s *UsersService) AddSSHKey(opt *AddSSHKeyOptions) (*SSHKey, *Response, error) { - req, err := s.client.NewRequest("POST", "user/keys", opt) +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#add-ssh-key +func (s *UsersService) AddSSHKey(opt *AddSSHKeyOptions, options ...OptionFunc) (*SSHKey, *Response, error) { + req, err := s.client.NewRequest("POST", "user/keys", opt, options) if err != nil { return nil, nil, err } @@ -324,13 +362,11 @@ func (s *UsersService) AddSSHKey(opt *AddSSHKeyOptions) (*SSHKey, *Response, err // AddSSHKeyForUser creates new key owned by specified user. Available only for // admin. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#add-ssh-key-for-user -func (s *UsersService) AddSSHKeyForUser( - user int, - opt *AddSSHKeyOptions) (*SSHKey, *Response, error) { +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#add-ssh-key-for-user +func (s *UsersService) AddSSHKeyForUser(user int, opt *AddSSHKeyOptions, options ...OptionFunc) (*SSHKey, *Response, error) { u := fmt.Sprintf("users/%d/keys", user) - req, err := s.client.NewRequest("POST", u, opt) + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } @@ -349,80 +385,467 @@ func (s *UsersService) AddSSHKeyForUser( // available results in 200 OK. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/users.html#delete-ssh-key-for-current-owner -func (s *UsersService) DeleteSSHKey(kid int) (*Response, error) { - u := fmt.Sprintf("user/keys/%d", kid) +// https://docs.gitlab.com/ce/api/users.html#delete-ssh-key-for-current-owner +func (s *UsersService) DeleteSSHKey(key int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("user/keys/%d", key) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteSSHKeyForUser deletes key owned by a specified user. Available only +// for admin. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#delete-ssh-key-for-given-user +func (s *UsersService) DeleteSSHKeyForUser(user, key int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("users/%d/keys/%d", user, key) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } + return s.client.Do(req, nil) +} + +// BlockUser blocks the specified user. Available only for admin. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#block-user +func (s *UsersService) BlockUser(user int, options ...OptionFunc) error { + u := fmt.Sprintf("users/%d/block", user) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return err + } + resp, err := s.client.Do(req, nil) if err != nil { - return resp, err + return err } - return resp, err + switch resp.StatusCode { + case 201: + return nil + case 403: + return errors.New("Cannot block a user that is already blocked by LDAP synchronization") + case 404: + return errors.New("User does not exist") + default: + return fmt.Errorf("Received unexpected result code: %d", resp.StatusCode) + } } -// DeleteSSHKeyForUser deletes key owned by a specified user. Available only +// UnblockUser unblocks the specified user. Available only for admin. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#unblock-user +func (s *UsersService) UnblockUser(user int, options ...OptionFunc) error { + u := fmt.Sprintf("users/%d/unblock", user) + + req, err := s.client.NewRequest("POST", u, nil, options) + if err != nil { + return err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return err + } + + switch resp.StatusCode { + case 201: + return nil + case 403: + return errors.New("Cannot unblock a user that is blocked by LDAP synchronization") + case 404: + return errors.New("User does not exist") + default: + return fmt.Errorf("Received unexpected result code: %d", resp.StatusCode) + } +} + +// Email represents an Email. +// +// GitLab API docs: https://doc.gitlab.com/ce/api/users.html#list-emails +type Email struct { + ID int `json:"id"` + Email string `json:"email"` +} + +// ListEmails gets a list of currently authenticated user's Emails. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#list-emails +func (s *UsersService) ListEmails(options ...OptionFunc) ([]*Email, *Response, error) { + req, err := s.client.NewRequest("GET", "user/emails", nil, options) + if err != nil { + return nil, nil, err + } + + var e []*Email + resp, err := s.client.Do(req, &e) + if err != nil { + return nil, resp, err + } + + return e, resp, err +} + +// ListEmailsForUserOptions represents the available ListEmailsForUser() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#list-emails-for-user +type ListEmailsForUserOptions ListOptions + +// ListEmailsForUser gets a list of a specified user's Emails. Available +// only for admin +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#list-emails-for-user +func (s *UsersService) ListEmailsForUser(user int, opt *ListEmailsForUserOptions, options ...OptionFunc) ([]*Email, *Response, error) { + u := fmt.Sprintf("users/%d/emails", user) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var e []*Email + resp, err := s.client.Do(req, &e) + if err != nil { + return nil, resp, err + } + + return e, resp, err +} + +// GetEmail gets a single email. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#single-email +func (s *UsersService) GetEmail(email int, options ...OptionFunc) (*Email, *Response, error) { + u := fmt.Sprintf("user/emails/%d", email) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + e := new(Email) + resp, err := s.client.Do(req, e) + if err != nil { + return nil, resp, err + } + + return e, resp, err +} + +// AddEmailOptions represents the available AddEmail() options. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#add-email +type AddEmailOptions struct { + Email *string `url:"email,omitempty" json:"email,omitempty"` +} + +// AddEmail creates a new email owned by the currently authenticated user. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#add-email +func (s *UsersService) AddEmail(opt *AddEmailOptions, options ...OptionFunc) (*Email, *Response, error) { + req, err := s.client.NewRequest("POST", "user/emails", opt, options) + if err != nil { + return nil, nil, err + } + + e := new(Email) + resp, err := s.client.Do(req, e) + if err != nil { + return nil, resp, err + } + + return e, resp, err +} + +// AddEmailForUser creates new email owned by specified user. Available only for +// admin. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/users.html#add-email-for-user +func (s *UsersService) AddEmailForUser(user int, opt *AddEmailOptions, options ...OptionFunc) (*Email, *Response, error) { + u := fmt.Sprintf("users/%d/emails", user) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + e := new(Email) + resp, err := s.client.Do(req, e) + if err != nil { + return nil, resp, err + } + + return e, resp, err +} + +// DeleteEmail deletes email owned by currently authenticated user. This is an +// idempotent function and calling it on a key that is already deleted or not +// available results in 200 OK. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#delete-email-for-current-owner +func (s *UsersService) DeleteEmail(email int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("user/emails/%d", email) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteEmailForUser deletes email owned by a specified user. Available only // for admin. // // GitLab API docs: -// http://doc.gitlab.com/ce/api/users.html#delete-ssh-key-for-given-user -func (s *UsersService) DeleteSSHKeyForUser(user int, kid int) (*Response, error) { - u := fmt.Sprintf("users/%d/keys/%d", user, kid) +// https://docs.gitlab.com/ce/api/users.html#delete-email-for-given-user +func (s *UsersService) DeleteEmailForUser(user, email int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("users/%d/emails/%d", user, email) - req, err := s.client.NewRequest("DELETE", u, nil) + req, err := s.client.NewRequest("DELETE", u, nil, options) if err != nil { return nil, err } - resp, err := s.client.Do(req, nil) + return s.client.Do(req, nil) +} + +// ImpersonationToken represents an impersonation token. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#get-all-impersonation-tokens-of-a-user +type ImpersonationToken struct { + ID int `json:"id"` + Name string `json:"name"` + Active bool `json:"active"` + Token string `json:"token"` + Scopes []string `json:"scopes"` + Revoked bool `json:"revoked"` + CreatedAt *time.Time `json:"created_at"` + ExpiresAt *ISOTime `json:"expires_at"` +} + +// GetAllImpersonationTokensOptions represents the available +// GetAllImpersonationTokens() options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#get-all-impersonation-tokens-of-a-user +type GetAllImpersonationTokensOptions struct { + ListOptions + State *string `url:"state,omitempty" json:"state,omitempty"` +} + +// GetAllImpersonationTokens retrieves all impersonation tokens of a user. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#get-all-impersonation-tokens-of-a-user +func (s *UsersService) GetAllImpersonationTokens(user int, opt *GetAllImpersonationTokensOptions, options ...OptionFunc) ([]*ImpersonationToken, *Response, error) { + u := fmt.Sprintf("users/%d/impersonation_tokens", user) + + req, err := s.client.NewRequest("GET", u, opt, options) if err != nil { - return resp, err + return nil, nil, err } - return resp, err + var ts []*ImpersonationToken + resp, err := s.client.Do(req, &ts) + if err != nil { + return nil, resp, err + } + + return ts, resp, err } -// BlockUser blocks the specified user. Available only for admin. +// GetImpersonationToken retrieves an impersonation token of a user. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#block-user -func (s *UsersService) BlockUser(user int) (*User, *Response, error) { - u := fmt.Sprintf("users/%d/block", user) +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#get-an-impersonation-token-of-a-user +func (s *UsersService) GetImpersonationToken(user, token int, options ...OptionFunc) (*ImpersonationToken, *Response, error) { + u := fmt.Sprintf("users/%d/impersonation_tokens/%d", user, token) - req, err := s.client.NewRequest("PUT", u, nil) + req, err := s.client.NewRequest("GET", u, nil, options) if err != nil { return nil, nil, err } - usr := new(User) - resp, err := s.client.Do(req, usr) + t := new(ImpersonationToken) + resp, err := s.client.Do(req, &t) if err != nil { return nil, resp, err } - return usr, resp, err + return t, resp, err } -// UnblockUser unblocks the specified user. Available only for admin. +// CreateImpersonationTokenOptions represents the available +// CreateImpersonationToken() options. // -// GitLab API docs: http://doc.gitlab.com/ce/api/users.html#unblock-user -func (s *UsersService) UnblockUser(user int) (*User, *Response, error) { - u := fmt.Sprintf("users/%d/unblock", user) +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#create-an-impersonation-token +type CreateImpersonationTokenOptions struct { + Name *string `url:"name,omitempty" json:"name,omitempty"` + Scopes *[]string `url:"scopes,omitempty" json:"scopes,omitempty"` + ExpiresAt *time.Time `url:"expires_at,omitempty" json:"expires_at,omitempty"` +} - req, err := s.client.NewRequest("PUT", u, nil) +// CreateImpersonationToken creates an impersonation token. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#create-an-impersonation-token +func (s *UsersService) CreateImpersonationToken(user int, opt *CreateImpersonationTokenOptions, options ...OptionFunc) (*ImpersonationToken, *Response, error) { + u := fmt.Sprintf("users/%d/impersonation_tokens", user) + + req, err := s.client.NewRequest("POST", u, opt, options) if err != nil { return nil, nil, err } - usr := new(User) - resp, err := s.client.Do(req, usr) + t := new(ImpersonationToken) + resp, err := s.client.Do(req, &t) if err != nil { return nil, resp, err } - return usr, resp, err + return t, resp, err +} + +// RevokeImpersonationToken revokes an impersonation token. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#revoke-an-impersonation-token +func (s *UsersService) RevokeImpersonationToken(user, token int, options ...OptionFunc) (*Response, error) { + u := fmt.Sprintf("users/%d/impersonation_tokens/%d", user, token) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// UserActivity represents an entry in the user/activities response +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#get-user-activities-admin-only +type UserActivity struct { + Username string `json:"username"` + LastActivityOn *ISOTime `json:"last_activity_on"` +} + +// GetUserActivitiesOptions represents the options for GetUserActivities +// +// GitLap API docs: +// https://docs.gitlab.com/ce/api/users.html#get-user-activities-admin-only +type GetUserActivitiesOptions struct { + From *ISOTime `url:"from,omitempty" json:"from,omitempty"` +} + +// GetUserActivities retrieves user activities (admin only) +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#get-user-activities-admin-only +func (s *UsersService) GetUserActivities(opt *GetUserActivitiesOptions, options ...OptionFunc) ([]*UserActivity, *Response, error) { + req, err := s.client.NewRequest("GET", "user/activities", opt, options) + if err != nil { + return nil, nil, err + } + + var t []*UserActivity + resp, err := s.client.Do(req, &t) + if err != nil { + return nil, resp, err + } + + return t, resp, err +} + +// UserStatus represents the current status of a user +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#user-status +type UserStatus struct { + Emoji string `json:"emoji"` + Message string `json:"message"` + MessageHTML string `json:"message_html"` +} + +// CurrentUserStatus retrieves the user status +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#user-status +func (s *UsersService) CurrentUserStatus(options ...OptionFunc) (*UserStatus, *Response, error) { + req, err := s.client.NewRequest("GET", "user/status", nil, options) + if err != nil { + return nil, nil, err + } + + status := new(UserStatus) + resp, err := s.client.Do(req, status) + if err != nil { + return nil, resp, err + } + + return status, resp, err +} + +// GetUserStatus retrieves a user's status +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#get-the-status-of-a-user +func (s *UsersService) GetUserStatus(user int, options ...OptionFunc) (*UserStatus, *Response, error) { + u := fmt.Sprintf("users/%d/status", user) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + status := new(UserStatus) + resp, err := s.client.Do(req, status) + if err != nil { + return nil, resp, err + } + + return status, resp, err +} + +// UserStatusOptions represents the options required to set the status +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#set-user-status +type UserStatusOptions struct { + Emoji *string `url:"emoji,omitempty" json:"emoji,omitempty"` + Message *string `url:"message,omitempty" json:"message,omitempty"` +} + +// SetUserStatus sets the user's status +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/users.html#set-user-status +func (s *UsersService) SetUserStatus(opt *UserStatusOptions, options ...OptionFunc) (*UserStatus, *Response, error) { + req, err := s.client.NewRequest("PUT", "user/status", opt, options) + if err != nil { + return nil, nil, err + } + + status := new(UserStatus) + resp, err := s.client.Do(req, status) + if err != nil { + return nil, resp, err + } + + return status, resp, err } diff --git a/api/validate.go b/api/validate.go new file mode 100644 index 0000000..a88e188 --- /dev/null +++ b/api/validate.go @@ -0,0 +1,40 @@ +package gitlab + +// ValidateService handles communication with the validation related methods of +// the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/lint.html +type ValidateService struct { + client *Client +} + +// LintResult represents the linting results. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/lint.html +type LintResult struct { + Status string `json:"status"` + Errors []string `json:"errors"` +} + +// Lint validates .gitlab-ci.yml content. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/lint.html +func (s *ValidateService) Lint(content string, options ...OptionFunc) (*LintResult, *Response, error) { + var opts struct { + Content string `url:"content,omitempty" json:"content,omitempty"` + } + opts.Content = content + + req, err := s.client.NewRequest("POST", "ci/lint", &opts, options) + if err != nil { + return nil, nil, err + } + + l := new(LintResult) + resp, err := s.client.Do(req, l) + if err != nil { + return nil, resp, err + } + + return l, resp, nil +} diff --git a/api/validate_test.go b/api/validate_test.go new file mode 100644 index 0000000..221fecf --- /dev/null +++ b/api/validate_test.go @@ -0,0 +1,71 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestValidate(t *testing.T) { + testCases := []struct { + description string + content string + response string + want *LintResult + }{ + { + description: "valid", + content: ` + build1: + stage: build + script: + - echo "Do your build here"`, + response: `{ + "status": "valid", + "errors": [] + }`, + want: &LintResult{ + Status: "valid", + Errors: []string{}, + }, + }, + { + description: "invalid", + content: ` + build1: + - echo "Do your build here"`, + response: `{ + "status": "invalid", + "errors": ["error message when content is invalid"] + }`, + want: &LintResult{ + Status: "invalid", + Errors: []string{"error message when content is invalid"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/ci/lint", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, tc.response) + }) + + got, _, err := client.Validate.Lint(tc.content) + + if err != nil { + t.Errorf("Validate returned error: %v", err) + } + + want := tc.want + if !reflect.DeepEqual(got, want) { + t.Errorf("Validate returned \ngot:\n%v\nwant:\n%v", Stringify(got), Stringify(want)) + } + }) + } +} diff --git a/api/version.go b/api/version.go new file mode 100644 index 0000000..f1a3a7f --- /dev/null +++ b/api/version.go @@ -0,0 +1,56 @@ +// +// Copyright 2017, Andrea Funto' +// +// 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 gitlab + +// VersionService handles communication with the GitLab server instance to +// retrieve its version information via the GitLab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/version.md +type VersionService struct { + client *Client +} + +// Version represents a GitLab instance version. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/version.md +type Version struct { + Version string `json:"version"` + Revision string `json:"revision"` +} + +func (s Version) String() string { + return Stringify(s) +} + +// GetVersion gets a GitLab server instance version; it is only available to +// authenticated users. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/version.md +func (s *VersionService) GetVersion() (*Version, *Response, error) { + req, err := s.client.NewRequest("GET", "version", nil, nil) + if err != nil { + return nil, nil, err + } + + v := new(Version) + resp, err := s.client.Do(req, v) + if err != nil { + return nil, resp, err + } + + return v, resp, err +} diff --git a/api/version_test.go b/api/version_test.go new file mode 100644 index 0000000..1383754 --- /dev/null +++ b/api/version_test.go @@ -0,0 +1,29 @@ +package gitlab + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestGetVersion(t *testing.T) { + mux, server, client := setup() + defer teardown(server) + + mux.HandleFunc("/api/v4/version", + func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"version":"11.3.4-ee", "revision":"14d3a1d"}`) + }) + + version, _, err := client.Version.GetVersion() + if err != nil { + t.Errorf("Version.GetVersion returned error: %v", err) + } + + want := &Version{Version: "11.3.4-ee", Revision: "14d3a1d"} + if !reflect.DeepEqual(want, version) { + t.Errorf("Version.GetVersion returned %+v, want %+v", version, want) + } +} diff --git a/api/wikis.go b/api/wikis.go new file mode 100644 index 0000000..7288985 --- /dev/null +++ b/api/wikis.go @@ -0,0 +1,204 @@ +// Copyright 2017, Stany MARCEL +// +// 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 gitlab + +import ( + "fmt" + "net/url" +) + +// WikisService handles communication with the wikis related methods of +// the Gitlab API. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/wikis.html +type WikisService struct { + client *Client +} + +// WikiFormat represents the available wiki formats. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/wikis.html +type WikiFormat string + +// The available wiki formats. +const ( + WikiFormatMarkdown WikiFormat = "markdown" + WikiFormatRFoc WikiFormat = "rdoc" + WikiFormatASCIIDoc WikiFormat = "asciidoc" +) + +// Wiki represents a GitLab wiki. +// +// GitLab API docs: https://docs.gitlab.com/ce/api/wikis.html +type Wiki struct { + Content string `json:"content"` + Format WikiFormat `json:"format"` + Slug string `json:"slug"` + Title string `json:"title"` +} + +func (w Wiki) String() string { + return Stringify(w) +} + +// ListWikisOptions represents the available ListWikis options. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/wikis.html#list-wiki-pages +type ListWikisOptions struct { + WithContent *bool `url:"with_content,omitempty" json:"with_content,omitempty"` +} + +// ListWikis lists all pages of the wiki of the given project id. +// When with_content is set, it also returns the content of the pages. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/wikis.html#list-wiki-pages +func (s *WikisService) ListWikis(pid interface{}, opt *ListWikisOptions, options ...OptionFunc) ([]*Wiki, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/wikis", url.QueryEscape(project)) + + req, err := s.client.NewRequest("GET", u, opt, options) + if err != nil { + return nil, nil, err + } + + var w []*Wiki + resp, err := s.client.Do(req, &w) + if err != nil { + return nil, resp, err + } + + return w, resp, err +} + +// GetWikiPage gets a wiki page for a given project. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/wikis.html#get-a-wiki-page +func (s *WikisService) GetWikiPage(pid interface{}, slug string, options ...OptionFunc) (*Wiki, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/wikis/%s", url.QueryEscape(project), url.QueryEscape(slug)) + + req, err := s.client.NewRequest("GET", u, nil, options) + if err != nil { + return nil, nil, err + } + + var w *Wiki + resp, err := s.client.Do(req, &w) + if err != nil { + return nil, resp, err + } + + return w, resp, err +} + +// CreateWikiPageOptions represents options to CreateWikiPage. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/wikis.html#create-a-new-wiki-page +type CreateWikiPageOptions struct { + Content *string `url:"content" json:"content"` + Title *string `url:"title" json:"title"` + Format *string `url:"format,omitempty" json:"format,omitempty"` +} + +// CreateWikiPage creates a new wiki page for the given repository with +// the given title, slug, and content. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/wikis.html#create-a-new-wiki-page +func (s *WikisService) CreateWikiPage(pid interface{}, opt *CreateWikiPageOptions, options ...OptionFunc) (*Wiki, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/wikis", url.QueryEscape(project)) + + req, err := s.client.NewRequest("POST", u, opt, options) + if err != nil { + return nil, nil, err + } + + w := new(Wiki) + resp, err := s.client.Do(req, w) + if err != nil { + return nil, resp, err + } + + return w, resp, err +} + +// EditWikiPageOptions represents options to EditWikiPage. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/wikis.html#edit-an-existing-wiki-page +type EditWikiPageOptions struct { + Content *string `url:"content" json:"content"` + Title *string `url:"title" json:"title"` + Format *string `url:"format,omitempty" json:"format,omitempty"` +} + +// EditWikiPage Updates an existing wiki page. At least one parameter is +// required to update the wiki page. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/wikis.html#edit-an-existing-wiki-page +func (s *WikisService) EditWikiPage(pid interface{}, slug string, opt *EditWikiPageOptions, options ...OptionFunc) (*Wiki, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/wikis/%s", url.QueryEscape(project), url.QueryEscape(slug)) + + req, err := s.client.NewRequest("PUT", u, opt, options) + if err != nil { + return nil, nil, err + } + + w := new(Wiki) + resp, err := s.client.Do(req, w) + if err != nil { + return nil, resp, err + } + + return w, resp, err +} + +// DeleteWikiPage deletes a wiki page with a given slug. +// +// GitLab API docs: +// https://docs.gitlab.com/ce/api/wikis.html#delete-a-wiki-page +func (s *WikisService) DeleteWikiPage(pid interface{}, slug string, options ...OptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/wikis/%s", url.QueryEscape(project), url.QueryEscape(slug)) + + req, err := s.client.NewRequest("DELETE", u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/gitlab.go b/gitlab.go index 51666cd..8469470 100644 --- a/gitlab.go +++ b/gitlab.go @@ -67,7 +67,7 @@ func chatSettings(c *integram.Context) ChatSettings { return s } -const apiSuffixURL = "/api/v3/" +const apiSuffixURL = "/api/v4/" // Service returns integram.Service from gitlab.Config func (c Config) Service() *integram.Service { @@ -406,7 +406,7 @@ func client(c *integram.Context) *api.Client { } func sendIssueComment(c *integram.Context, projectID int, issueID int, text string) error { - note, _, err := client(c).Notes.CreateIssueNote(projectID, issueID, &api.CreateIssueNoteOptions{Body: text}) + note, _, err := client(c).Notes.CreateIssueNote(projectID, issueID, &api.CreateIssueNoteOptions{Body: &text}) if note != nil { c.Message.UpdateEventsID(c.Db(), "issue_note_"+strconv.Itoa(note.ID)) @@ -416,7 +416,7 @@ func sendIssueComment(c *integram.Context, projectID int, issueID int, text stri } func sendMRComment(c *integram.Context, projectID int, MergeRequestID int, text string) error { - note, _, err := client(c).Notes.CreateMergeRequestNote(projectID, MergeRequestID, &api.CreateMergeRequestNoteOptions{Body: text}) + note, _, err := client(c).Notes.CreateMergeRequestNote(projectID, MergeRequestID, &api.CreateMergeRequestNoteOptions{Body: &text}) if note != nil { c.Message.UpdateEventsID(c.Db(), noteUniqueID(projectID, strconv.Itoa(note.ID))) @@ -426,7 +426,7 @@ func sendMRComment(c *integram.Context, projectID int, MergeRequestID int, text } func sendSnippetComment(c *integram.Context, projectID int, SnippetID int, text string) error { - note, _, err := client(c).Notes.CreateSnippetNote(projectID, SnippetID, &api.CreateSnippetNoteOptions{Body: text}) + note, _, err := client(c).Notes.CreateSnippetNote(projectID, SnippetID, &api.CreateSnippetNoteOptions{Body: &text}) if note != nil { c.Message.UpdateEventsID(c.Db(), noteUniqueID(projectID, strconv.Itoa(note.ID))) } @@ -442,7 +442,7 @@ func trim(s string, max int) string { } func sendCommitComment(c *integram.Context, projectID int, commitID string, msg *integram.IncomingMessage) error { - note, _, err := client(c).Notes.CreateCommitNote(projectID, commitID, &api.CreateCommitNoteOptions{Note: msg.Text}) + note, _, err := client(c).Notes.CreateCommitNote(projectID, commitID, &api.CreateCommitNoteOptions{Note: &msg.Text}) if err != nil { return err } @@ -711,7 +711,7 @@ func webhookHandler(c *integram.Context, request *integram.WebhookContext) (err return nil } - msg.SetReplyAction(issueReplied, c.ServiceBaseURL.String(), wh.ObjectAttributes.ProjectID, wh.ObjectAttributes.ID) + msg.SetReplyAction(issueReplied, c.ServiceBaseURL.String(), wh.ObjectAttributes.ProjectID, wh.ObjectAttributes.Iid) if wh.ObjectAttributes.Action == "open" { return msg.AddEventID("issue_" + strconv.Itoa(wh.ObjectAttributes.ID)).SetText(fmt.Sprintf("%s %s %s at %s:\n%s\n%s", mention(c, wh.User.Username, wh.UserEmail), wh.ObjectAttributes.State, m.URL("issue", wh.ObjectAttributes.URL), m.URL(wh.User.Username+" / "+wh.Repository.Name, wh.Repository.Homepage), m.Bold(wh.ObjectAttributes.Title), wh.ObjectAttributes.Description)).