diff --git a/commits.go b/commits.go index 0c597d863..cdb548b07 100644 --- a/commits.go +++ b/commits.go @@ -77,6 +77,7 @@ type ListCommitsOptions struct { 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"` + Author *string `url:"author,omitempty" json:"author,omitempty"` All *bool `url:"all,omitempty" json:"all,omitempty"` WithStats *bool `url:"with_stats,omitempty" json:"with_stats,omitempty"` FirstParent *bool `url:"first_parent,omitempty" json:"first_parent,omitempty"` @@ -402,6 +403,7 @@ type CommitStatus struct { Name string `json:"name"` AllowFailure bool `json:"allow_failure"` Coverage float64 `json:"coverage"` + PipelineId int `json:"pipeline_id"` Author Author `json:"author"` Description string `json:"description"` TargetURL string `json:"target_url"` diff --git a/examples/repository_archive.go b/examples/repository_archive.go new file mode 100644 index 000000000..97fe43c56 --- /dev/null +++ b/examples/repository_archive.go @@ -0,0 +1,42 @@ +// +// Copyright 2021, 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 main + +import ( + "log" + + "github.com/xanzy/go-gitlab" +) + +func repositoryArchiveExample() { + git, err := gitlab.NewClient("yourtokengoeshere") + if err != nil { + log.Fatal(err) + } + + // Get repository archive + opt := &gitlab.ArchiveOptions{ + Format: gitlab.String("tar.gz"), + Path: gitlab.String("mydir"), + } + content, _, err := git.Repositories.Archive("mygroup/myproject", opt, nil) + if err != nil { + log.Fatal(err) + } + + log.Printf("Repository archive contains %d byte(s)", len(content)) +} diff --git a/examples/services.go b/examples/services.go new file mode 100644 index 000000000..029398bfb --- /dev/null +++ b/examples/services.go @@ -0,0 +1,63 @@ +// +// Copyright 2023, Joel Gerber +// +// 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 main + +import ( + "fmt" + "log" + + "github.com/xanzy/go-gitlab" +) + +func dataDogExample() { + git, err := gitlab.NewClient("yourtokengoeshere") + if err != nil { + log.Fatal(err) + } + + // Create new DataDog integration + opts := &gitlab.SetDataDogServiceOptions{ + APIKey: gitlab.String("testing"), + DataDogEnv: gitlab.String("sandbox"), + DataDogService: gitlab.String("test"), + DataDogSite: gitlab.String("datadoghq.com"), + DataDogTags: gitlab.String("country:canada\nprovince:ontario"), + ArchiveTraceEvents: gitlab.Bool(true), + } + + _, err = git.Services.SetDataDogService(1, opts) + if err != nil { + log.Fatal(err) + } + + // Query the integration + svc, _, err := git.Services.GetDataDogService(1) + if err != nil { + log.Fatal(err) + } + fmt.Printf( + "api_url: %s, datadog_env: %s, datadog_service: %s, datadog_site: %s, datadog_tags: %s", + svc.Properties.APIURL, svc.Properties.DataDogEnv, svc.Properties.DataDogService, + svc.Properties.DataDogSite, svc.Properties.DataDogTags, + ) + + // Delete the integration + _, err = git.Services.DeleteDataDogService(1) + if err != nil { + log.Fatal(err) + } +} diff --git a/group_access_tokens.go b/group_access_tokens.go index 3d42ab531..15f303a98 100644 --- a/group_access_tokens.go +++ b/group_access_tokens.go @@ -145,6 +145,31 @@ func (s *GroupAccessTokensService) CreateGroupAccessToken(gid interface{}, opt * return pat, resp, nil } +// RotateGroupAccessToken revokes a group access token and returns a new group +// access token that expires in one week. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/group_access_tokens.html#rotate-a-group-access-token +func (s *GroupAccessTokensService) RotateGroupAccessToken(gid interface{}, id int, options ...RequestOptionFunc) (*GroupAccessToken, *Response, error) { + groups, err := parseID(gid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("groups/%s/access_tokens/%d/rotate", PathEscape(groups), id) + req, err := s.client.NewRequest(http.MethodPost, u, nil, options) + if err != nil { + return nil, nil, err + } + + gat := new(GroupAccessToken) + resp, err := s.client.Do(req, gat) + if err != nil { + return nil, resp, err + } + + return gat, resp, nil +} + // RevokeGroupAccessToken revokes a group access token. // // GitLab API docs: diff --git a/merge_requests.go b/merge_requests.go index 822b0ded6..1df07225f 100644 --- a/merge_requests.go +++ b/merge_requests.go @@ -36,59 +36,50 @@ type MergeRequestsService struct { // // GitLab API docs: https://docs.gitlab.com/ee/api/merge_requests.html type MergeRequest struct { - 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 *BasicUser `json:"author"` - Assignee *BasicUser `json:"assignee"` - Assignees []*BasicUser `json:"assignees"` - Reviewers []*BasicUser `json:"reviewers"` - SourceProjectID int `json:"source_project_id"` - TargetProjectID int `json:"target_project_id"` - Labels Labels `json:"labels"` - LabelDetails []*LabelDetails `json:"label_details"` - Description string `json:"description"` - Draft bool `json:"draft"` - WorkInProgress bool `json:"work_in_progress"` - Milestone *Milestone `json:"milestone"` - MergeWhenPipelineSucceeds bool `json:"merge_when_pipeline_succeeds"` - DetailedMergeStatus string `json:"detailed_merge_status"` - MergeError string `json:"merge_error"` - MergedBy *BasicUser `json:"merged_by"` - MergedAt *time.Time `json:"merged_at"` - ClosedBy *BasicUser `json:"closed_by"` - ClosedAt *time.Time `json:"closed_at"` - Subscribed bool `json:"subscribed"` - SHA string `json:"sha"` - MergeCommitSHA string `json:"merge_commit_sha"` - SquashCommitSHA string `json:"squash_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"` - AllowCollaboration bool `json:"allow_collaboration"` - WebURL string `json:"web_url"` - References *IssueReferences `json:"references"` - DiscussionLocked bool `json:"discussion_locked"` - Changes []struct { - OldPath string `json:"old_path"` - NewPath string `json:"new_path"` - AMode string `json:"a_mode"` - BMode string `json:"b_mode"` - Diff string `json:"diff"` - NewFile bool `json:"new_file"` - RenamedFile bool `json:"renamed_file"` - DeletedFile bool `json:"deleted_file"` - } `json:"changes"` - User struct { + 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 *BasicUser `json:"author"` + Assignee *BasicUser `json:"assignee"` + Assignees []*BasicUser `json:"assignees"` + Reviewers []*BasicUser `json:"reviewers"` + SourceProjectID int `json:"source_project_id"` + TargetProjectID int `json:"target_project_id"` + Labels Labels `json:"labels"` + LabelDetails []*LabelDetails `json:"label_details"` + Description string `json:"description"` + Draft bool `json:"draft"` + WorkInProgress bool `json:"work_in_progress"` + Milestone *Milestone `json:"milestone"` + MergeWhenPipelineSucceeds bool `json:"merge_when_pipeline_succeeds"` + DetailedMergeStatus string `json:"detailed_merge_status"` + MergeError string `json:"merge_error"` + MergedBy *BasicUser `json:"merged_by"` + MergedAt *time.Time `json:"merged_at"` + ClosedBy *BasicUser `json:"closed_by"` + ClosedAt *time.Time `json:"closed_at"` + Subscribed bool `json:"subscribed"` + SHA string `json:"sha"` + MergeCommitSHA string `json:"merge_commit_sha"` + SquashCommitSHA string `json:"squash_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"` + AllowCollaboration bool `json:"allow_collaboration"` + WebURL string `json:"web_url"` + References *IssueReferences `json:"references"` + DiscussionLocked bool `json:"discussion_locked"` + Changes []*MergeRequestDiff `json:"changes"` + User struct { CanMerge bool `json:"can_merge"` } `json:"user"` TimeStats *TimeStats `json:"time_stats"` @@ -152,6 +143,21 @@ func (m *MergeRequest) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, (*alias)(m)) } +// MergeRequestDiff represents Gitlab merge request diff. +// +// Gitlab API docs: +// https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-request-diffs +type MergeRequestDiff struct { + OldPath string `json:"old_path"` + NewPath string `json:"new_path"` + AMode string `json:"a_mode"` + BMode string `json:"b_mode"` + Diff string `json:"diff"` + NewFile bool `json:"new_file"` + RenamedFile bool `json:"renamed_file"` + DeletedFile bool `json:"deleted_file"` +} + // MergeRequestDiffVersion represents Gitlab merge request version. // // Gitlab API docs: @@ -461,6 +467,9 @@ type GetMergeRequestChangesOptions struct { // GetMergeRequestChanges shows information about the merge request including // its files and changes. // +// Deprecated: This endpoint has been replaced by +// MergeRequestsService.ListMergeRequesDiffs() +// // GitLab API docs: // https://docs.gitlab.com/ee/api/merge_requests.html#get-single-merge-request-changes func (s *MergeRequestsService) GetMergeRequestChanges(pid interface{}, mergeRequest int, opt *GetMergeRequestChangesOptions, options ...RequestOptionFunc) (*MergeRequest, *Response, error) { @@ -484,6 +493,39 @@ func (s *MergeRequestsService) GetMergeRequestChanges(pid interface{}, mergeRequ return m, resp, nil } +// ListMergeRequesDiffsOptions represents the available ListMergeRequesDiffs() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-request-diffs +type ListMergeRequesDiffsOptions ListOptions + +// ListMergeRequesDiffs List diffs of the files changed in a merge request +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-request-diffs +func (s *MergeRequestsService) ListMergeRequesDiffs(pid interface{}, mergeRequest int, opt *ListMergeRequesDiffsOptions, options ...RequestOptionFunc) ([]*MergeRequestDiff, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + + u := fmt.Sprintf("/projects/%s/merge_requests/%d/diffs", PathEscape(project), mergeRequest) + + req, err := s.client.NewRequest(http.MethodGet, u, opt, options) + if err != nil { + return nil, nil, err + } + + var m []*MergeRequestDiff + resp, err := s.client.Do(req, &m) + if err != nil { + return nil, resp, err + } + + return m, resp, nil +} + // GetMergeRequestParticipants gets a list of merge request participants. // // GitLab API docs: @@ -500,13 +542,48 @@ func (s *MergeRequestsService) GetMergeRequestParticipants(pid interface{}, merg return nil, nil, err } - var ps []*BasicUser - resp, err := s.client.Do(req, &ps) + var bu []*BasicUser + resp, err := s.client.Do(req, &bu) + if err != nil { + return nil, resp, err + } + + return bu, resp, nil +} + +// MergeRequestReviewer represents a GitLab merge request reviewer. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_requests.html#get-single-merge-request-reviewers +type MergeRequestReviewer struct { + User *BasicUser `json:"user"` + State string `json:"state"` + CreatedAt *time.Time `json:"created_at"` +} + +// GetMergeRequestReviewers gets a list of merge request reviewers. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/merge_requests.html#get-single-merge-request-reviewers +func (s *MergeRequestsService) GetMergeRequestReviewers(pid interface{}, mergeRequest int, options ...RequestOptionFunc) ([]*MergeRequestReviewer, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/merge_requests/%d/reviewers", PathEscape(project), mergeRequest) + + req, err := s.client.NewRequest(http.MethodGet, u, nil, options) + if err != nil { + return nil, nil, err + } + + var mrr []*MergeRequestReviewer + resp, err := s.client.Do(req, &mrr) if err != nil { return nil, resp, err } - return ps, resp, nil + return mrr, resp, nil } // ListMergeRequestPipelines gets all pipelines for the provided merge request. diff --git a/merge_requests_test.go b/merge_requests_test.go index ee6452bf3..f674daa3e 100644 --- a/merge_requests_test.go +++ b/merge_requests_test.go @@ -245,6 +245,34 @@ func TestGetMergeRequestParticipants(t *testing.T) { } } +func TestGetMergeRequestReviewers(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/merge_requests/5/reviewers", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + testURL(t, r, "/api/v4/projects/1/merge_requests/5/reviewers") + + fmt.Fprint(w, `[{"user":{"id":1,"name":"John Doe1","username":"user1","state":"active","avatar_url":"http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon","web_url":"http://localhost/user1"},"state":"unreviewed","created_at":"2022-07-27T17:03:27.684Z"},{"user":{"id":2,"name":"John Doe2","username":"user2","state":"active","avatar_url":"http://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80&d=identicon","web_url":"http://localhost/user2"},"state":"reviewed","created_at":"2022-07-27T17:03:27.684Z"}]`) + }) + + mergeRequestReviewers, _, err := client.MergeRequests.GetMergeRequestReviewers("1", 5) + if err != nil { + log.Fatal(err) + } + + createdAt := time.Date(2022, 07, 27, 17, 3, 27, 684000000, time.UTC) + user1 := BasicUser{ID: 1, Name: "John Doe1", Username: "user1", State: "active", AvatarURL: "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", WebURL: "http://localhost/user1"} + user2 := BasicUser{ID: 2, Name: "John Doe2", Username: "user2", State: "active", AvatarURL: "http://www.gravatar.com/avatar/10fc7f102be8de7657fb4d80898bbfe3?s=80&d=identicon", WebURL: "http://localhost/user2"} + + assert.Len(t, mergeRequestReviewers, 2) + assert.Equal(t, "unreviewed", mergeRequestReviewers[0].State) + require.Equal(t, &user1, mergeRequestReviewers[0].User) + require.Equal(t, &createdAt, mergeRequestReviewers[0].CreatedAt) + assert.Equal(t, "reviewed", mergeRequestReviewers[1].State) + require.Equal(t, &user2, mergeRequestReviewers[1].User) + require.Equal(t, &createdAt, mergeRequestReviewers[1].CreatedAt) +} + func TestGetIssuesClosedOnMerge_Jira(t *testing.T) { mux, client := setup(t) mux.HandleFunc("/api/v4/projects/1/merge_requests/1/closes_issues", func(w http.ResponseWriter, r *http.Request) { @@ -260,6 +288,52 @@ func TestGetIssuesClosedOnMerge_Jira(t *testing.T) { assert.Equal(t, "Title of this issue", issues[0].Title) } +func TestListMergeRequesDiffs(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/merge_requests/1/diffs", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + mustWriteHTTPResponse(t, w, "testdata/list_merge_request_diff.json") + }) + + opts := &ListMergeRequesDiffsOptions{ + Page: 1, + PerPage: 2, + } + + diffs, _, err := client.MergeRequests.ListMergeRequesDiffs(1, 1, opts) + if err != nil { + t.Errorf("MergeRequests.ListMergeRequesDiffs returned error: %v", err) + } + + want := []*MergeRequestDiff{ + { + OldPath: "README", + NewPath: "README", + AMode: "100644", + BMode: "100644", + Diff: "@@ -1 +1 @@ -Title +README", + NewFile: false, + RenamedFile: false, + DeletedFile: false, + }, + { + OldPath: "VERSION", + NewPath: "VERSION", + AMode: "100644", + BMode: "100644", + Diff: "@@ -1.9.7 +1.9.8", + NewFile: false, + RenamedFile: false, + DeletedFile: false, + }, + } + + if !reflect.DeepEqual(want, diffs) { + t.Errorf("MergeRequests.ListMergeRequesDiffs returned %+v, want %+v", diffs, want) + } +} + func TestIntSliceOrString(t *testing.T) { t.Run("any", func(t *testing.T) { opts := &ListMergeRequestsOptions{} diff --git a/project_access_tokens.go b/project_access_tokens.go index 9fa510d09..780e59a82 100644 --- a/project_access_tokens.go +++ b/project_access_tokens.go @@ -146,6 +146,31 @@ func (s *ProjectAccessTokensService) CreateProjectAccessToken(pid interface{}, o return pat, resp, nil } +// RotateProjectAccessToken revokes a project access token and returns a new +// project access token that expires in one week. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/project_access_tokens.html#rotate-a-project-access-token +func (s *ProjectAccessTokensService) RotateProjectAccessToken(pid interface{}, id int, options ...RequestOptionFunc) (*ProjectAccessToken, *Response, error) { + projects, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/access_tokens/%d/rotate", PathEscape(projects), id) + req, err := s.client.NewRequest(http.MethodPost, u, nil, options) + if err != nil { + return nil, nil, err + } + + pat := new(ProjectAccessToken) + resp, err := s.client.Do(req, pat) + if err != nil { + return nil, resp, err + } + + return pat, resp, nil +} + // RevokeProjectAccessToken revokes a project access token. // // GitLab API docs: diff --git a/projects.go b/projects.go index 6d4cfe77a..94ef55582 100644 --- a/projects.go +++ b/projects.go @@ -390,6 +390,31 @@ func (s *ProjectsService) ListUserProjects(uid interface{}, opt *ListProjectsOpt return p, resp, nil } +// ListUserContributedProjects gets a list of visible projects a given user has contributed to. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/projects.html#list-projects-a-user-has-contributed-to +func (s *ProjectsService) ListUserContributedProjects(uid interface{}, opt *ListProjectsOptions, options ...RequestOptionFunc) ([]*Project, *Response, error) { + user, err := parseID(uid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("users/%s/contributed_projects", user) + + req, err := s.client.NewRequest(http.MethodGet, u, opt, options) + if err != nil { + return nil, nil, err + } + + var p []*Project + resp, err := s.client.Do(req, &p) + if err != nil { + return nil, resp, err + } + + return p, resp, nil +} + // ListUserStarredProjects gets a list of projects starred by the given user. // // GitLab API docs: @@ -1809,6 +1834,7 @@ func (s *ProjectsService) GetProjectApprovalRule(pid interface{}, ruleID int, op type CreateProjectLevelRuleOptions struct { Name *string `url:"name,omitempty" json:"name,omitempty"` ApprovalsRequired *int `url:"approvals_required,omitempty" json:"approvals_required,omitempty"` + ReportType *string `url:"report_type,omitempty" json:"report_type,omitempty"` RuleType *string `url:"rule_type,omitempty" json:"rule_type,omitempty"` UserIDs *[]int `url:"user_ids,omitempty" json:"user_ids,omitempty"` GroupIDs *[]int `url:"group_ids,omitempty" json:"group_ids,omitempty"` diff --git a/projects_test.go b/projects_test.go index 7500357a4..cf4965a75 100644 --- a/projects_test.go +++ b/projects_test.go @@ -88,6 +88,35 @@ func TestListUserProjects(t *testing.T) { } } +func TestListUserContributedProjects(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/users/1/contributed_projects", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + 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.ListUserContributedProjects(1, opt) + if err != nil { + t.Errorf("Projects.ListUserContributedProjects returned error: %v", err) + } + + want := []*Project{{ID: 1}, {ID: 2}} + if !reflect.DeepEqual(want, projects) { + t.Errorf("Projects.ListUserContributedProjects returned %+v, want %+v", projects, want) + } +} + func TestListUserStarredProjects(t *testing.T) { mux, client := setup(t) @@ -1194,6 +1223,7 @@ func TestCreateProjectApprovalRule(t *testing.T) { ApprovalsRequired: Int(3), UserIDs: &[]int{5, 50}, GroupIDs: &[]int{5}, + ReportType: String("code_coverage"), } rule, _, err := client.Projects.CreateProjectApprovalRule(1, opt) diff --git a/releases.go b/releases.go index 86308c5a9..3d6a89772 100644 --- a/releases.go +++ b/releases.go @@ -133,6 +133,31 @@ func (s *ReleasesService) GetRelease(pid interface{}, tagName string, options .. return r, resp, nil } +// GetLatestRelease returns the latest release for the project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/releases/#get-the-latest-release +func (s *ReleasesService) GetLatestRelease(pid interface{}, options ...RequestOptionFunc) (*Release, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/releases/permalink/latest", PathEscape(project)) + + req, err := s.client.NewRequest(http.MethodGet, u, nil, 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 +} + // CreateReleaseOptions represents CreateRelease() options. // // GitLab API docs: diff --git a/repositories.go b/repositories.go index b89efc090..986dd001c 100644 --- a/repositories.go +++ b/repositories.go @@ -140,6 +140,7 @@ func (s *RepositoriesService) RawBlobContent(pid interface{}, sha string, option // https://docs.gitlab.com/ee/api/repositories.html#get-file-archive type ArchiveOptions struct { Format *string `url:"-" json:"-"` + Path *string `url:"path,omitempty" json:"path,omitempty"` SHA *string `url:"sha,omitempty" json:"sha,omitempty"` } diff --git a/services.go b/services.go index 69842aec1..0313eccdb 100644 --- a/services.go +++ b/services.go @@ -179,6 +179,106 @@ func (s *ServicesService) DeleteCustomIssueTrackerService(pid interface{}, optio return s.client.Do(req, nil) } +// DataDogService represents DataDog service settings. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/services.html#datadog +type DataDogService struct { + Service + Properties *DataDogServiceProperties `json:"properties"` +} + +// DataDogServiceProperties represents DataDog specific properties. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/services.html#datadog +type DataDogServiceProperties struct { + APIURL string `url:"api_url,omitempty" json:"api_url,omitempty"` + DataDogEnv string `url:"datadog_env,omitempty" json:"datadog_env,omitempty"` + DataDogService string `url:"datadog_service,omitempty" json:"datadog_service,omitempty"` + DataDogSite string `url:"datadog_site,omitempty" json:"datadog_site,omitempty"` + DataDogTags string `url:"datadog_tags,omitempty" json:"datadog_tags,omitempty"` + ArchiveTraceEvents bool `url:"archive_trace_events,omitempty" json:"archive_trace_events,omitempty"` +} + +// GetDataDogService gets DataDog service settings for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/services.html#get-datadog-integration-settings +func (s *ServicesService) GetDataDogService(pid interface{}, options ...RequestOptionFunc) (*DataDogService, *Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, nil, err + } + u := fmt.Sprintf("projects/%s/services/datadog", PathEscape(project)) + + req, err := s.client.NewRequest(http.MethodGet, u, nil, options) + if err != nil { + return nil, nil, err + } + + svc := new(DataDogService) + resp, err := s.client.Do(req, svc) + if err != nil { + return nil, resp, err + } + + return svc, resp, nil +} + +// SetDataDogServiceOptions represents the available SetDataDogService() +// options. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/services.html#createedit-datadog-integration +type SetDataDogServiceOptions struct { + APIKey *string `url:"api_key,omitempty" json:"api_key,omitempty"` + APIURL *string `url:"api_url,omitempty" json:"api_url,omitempty"` + DataDogEnv *string `url:"datadog_env,omitempty" json:"datadog_env,omitempty"` + DataDogService *string `url:"datadog_service,omitempty" json:"datadog_service,omitempty"` + DataDogSite *string `url:"datadog_site,omitempty" json:"datadog_site,omitempty"` + DataDogTags *string `url:"datadog_tags,omitempty" json:"datadog_tags,omitempty"` + ArchiveTraceEvents *bool `url:"archive_trace_events,omitempty" json:"archive_trace_events,omitempty"` +} + +// SetDataDogService sets DataDog service settings for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/services.html#createedit-datadog-integration +func (s *ServicesService) SetDataDogService(pid interface{}, opt *SetDataDogServiceOptions, options ...RequestOptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/datadog", PathEscape(project)) + + req, err := s.client.NewRequest(http.MethodPut, u, opt, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// DeleteDataDogService deletes the DataDog service settings for a project. +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/services.html#disable-datadog-integration +func (s *ServicesService) DeleteDataDogService(pid interface{}, options ...RequestOptionFunc) (*Response, error) { + project, err := parseID(pid) + if err != nil { + return nil, err + } + u := fmt.Sprintf("projects/%s/services/datadog", PathEscape(project)) + + req, err := s.client.NewRequest(http.MethodDelete, u, nil, options) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + // DiscordService represents Discord service settings. // // GitLab API docs: diff --git a/services_test.go b/services_test.go index d2452f906..e2b6836af 100644 --- a/services_test.go +++ b/services_test.go @@ -105,6 +105,71 @@ func TestDeleteCustomIssueTrackerService(t *testing.T) { } } +func TestGetDataDogService(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/services/datadog", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, `{"id": 1, "active": true, "properties": {"datadog_site":"datadoghq.com", "api_url": "", "archive_trace_events": true, "datadog_service": "gitlab", "datadog_env": "production", "datadog_tags": "country=canada\nprovince=ontario"}}`) + }) + want := &DataDogService{ + Service: Service{ID: 1, Active: true}, + Properties: &DataDogServiceProperties{ + APIURL: "", + ArchiveTraceEvents: true, + DataDogEnv: "production", + DataDogService: "gitlab", + DataDogSite: "datadoghq.com", + DataDogTags: "country=canada\nprovince=ontario", + }, + } + + service, _, err := client.Services.GetDataDogService(1) + if err != nil { + t.Fatalf("Services.GetDataDogService returns an error: %v", err) + } + if !reflect.DeepEqual(want, service) { + t.Errorf("Services.GetDataDogService returned %+v, want %+v", service, want) + } +} + +func TestSetDataDogService(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/services/datadog", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodPut) + testBody(t, r, `{"api_key":"secret","api_url":"https://some-api.com","archive_trace_events":false,"datadog_env":"sandbox","datadog_service":"source-code","datadog_site":"datadoghq.eu","datadog_tags":"country=france"}`) + }) + + opt := &SetDataDogServiceOptions{ + APIKey: String("secret"), + APIURL: String("https://some-api.com"), + ArchiveTraceEvents: Bool(false), + DataDogEnv: String("sandbox"), + DataDogService: String("source-code"), + DataDogSite: String("datadoghq.eu"), + DataDogTags: String("country=france"), + } + + _, err := client.Services.SetDataDogService(1, opt) + if err != nil { + t.Fatalf("Services.SetDataDogService returns an error: %v", err) + } +} + +func TestDeleteDataDogService(t *testing.T) { + mux, client := setup(t) + + mux.HandleFunc("/api/v4/projects/1/services/datadog", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + }) + + _, err := client.Services.DeleteDataDogService(1) + if err != nil { + t.Fatalf("Services.DeleteDataDogService returns an error: %v", err) + } +} + func TestGetDiscordService(t *testing.T) { mux, client := setup(t) diff --git a/testdata/list_merge_request_diff.json b/testdata/list_merge_request_diff.json new file mode 100644 index 000000000..9b8e9da37 --- /dev/null +++ b/testdata/list_merge_request_diff.json @@ -0,0 +1,22 @@ +[ + { + "old_path": "README", + "new_path": "README", + "a_mode": "100644", + "b_mode": "100644", + "diff": "@@ -1 +1 @@ -Title +README", + "new_file": false, + "renamed_file": false, + "deleted_file": false + }, + { + "old_path": "VERSION", + "new_path": "VERSION", + "a_mode": "100644", + "b_mode": "100644", + "diff": "@@ -1.9.7 +1.9.8", + "new_file": false, + "renamed_file": false, + "deleted_file": false + } +]