Skip to content
This repository was archived by the owner on Dec 10, 2024. It is now read-only.
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: xanzy/go-gitlab
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.101.0
Choose a base ref
...
head repository: xanzy/go-gitlab
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.102.0
Choose a head ref

Commits on Mar 29, 2024

  1. Copy the full SHA
    30afa20 View commit details
  2. Copy the full SHA
    2393bad View commit details

Commits on Mar 30, 2024

  1. Added draft note

    harrisoncramer committed Mar 30, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    5071e30 View commit details
  2. Copy the full SHA
    4f39e92 View commit details
  3. Copy the full SHA
    15ef837 View commit details
  4. Copy the full SHA
    394e800 View commit details
  5. Copy the full SHA
    dd468ec View commit details
  6. Formatting

    harrisoncramer committed Mar 30, 2024
    Copy the full SHA
    8c9df79 View commit details

Commits on Apr 2, 2024

  1. Copy the full SHA
    be6bc9f View commit details

Commits on Apr 3, 2024

  1. Add new custom role permissions, allow GET to retrieve MemberRole

    RicePatrick committed Apr 3, 2024
    Copy the full SHA
    ffae827 View commit details
  2. Copy the full SHA
    f0156f0 View commit details
  3. Fix typos on a couple JSON tags

    RicePatrick committed Apr 3, 2024
    Copy the full SHA
    f6963b8 View commit details
  4. Add support for EmailsEnabled to groups

    RicePatrick committed Apr 3, 2024
    Copy the full SHA
    21750f1 View commit details
  5. Add proper colons to Deprecated message

    RicePatrick committed Apr 3, 2024
    Copy the full SHA
    48382c2 View commit details

Commits on Apr 5, 2024

  1. Merge pull request #1909 from PatrickRice-KSC/add-support-for-emails-…

    …enabled-to-group
    
    Add support for EmailsEnabled to groups
    svanharmelen authored Apr 5, 2024
    Copy the full SHA
    3939b35 View commit details
  2. Merge pull request #1908 from PatrickRice-KSC/CusomRoleUpdates

    Add new custom role permissions, allow GET to retrieve MemberRole
    svanharmelen authored Apr 5, 2024
    Copy the full SHA
    1ccfef0 View commit details
  3. Order the imports using goimports

    svanharmelen committed Apr 5, 2024
    Copy the full SHA
    f21d2ce View commit details
  4. Merge pull request #1907 from dieterdemeyer/service-account-token-exp…

    …iration-support
    
    Add support for service account token expiration
    svanharmelen authored Apr 5, 2024
    Copy the full SHA
    05c7374 View commit details
  5. Fixes #1904

    svanharmelen committed Apr 5, 2024
    Copy the full SHA
    af86d97 View commit details
  6. Merge pull request #1910 from xanzy/fix/updated-by-id

    Fixes updated_by_id in the webhook event
    svanharmelen authored Apr 5, 2024
    Copy the full SHA
    0d20034 View commit details
  7. Just a few minor style tweaks

    svanharmelen committed Apr 5, 2024
    Copy the full SHA
    f81ec4e View commit details
  8. Merge pull request #1905 from harrisoncramer/1861-add-drafts-support

    Add support for "draft" notes
    svanharmelen authored Apr 5, 2024
    Copy the full SHA
    1fb53be View commit details
233 changes: 233 additions & 0 deletions draft_notes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
//
// 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 gitlab

import (
"fmt"
"net/http"
)

type DraftNote struct {
ID int `json:"id"`
AuthorID int `json:"author_id"`
MergeRequestID int `json:"merge_request_id"`
ResolveDiscussion bool `json:"resolve_discussion"`
DiscussionID string `json:"discussion_id"`
Note string `json:"note"`
CommitID string `json:"commit_id"`
LineCode string `json:"line_code"`
Position *NotePosition `json:"position"`
}

// DraftNotesService handles communication with the draft notes related methods
// of the GitLab API.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/draft_notes.html#list-all-merge-request-draft-notes
type DraftNotesService struct {
client *Client
}

// ListDraftNotesOptions represents the available ListDraftNotes()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/draft_notes.html#list-all-merge-request-draft-notes
type ListDraftNotesOptions struct {
ListOptions
OrderBy *string `url:"order_by,omitempty" json:"order_by,omitempty"`
Sort *string `url:"sort,omitempty" json:"sort,omitempty"`
}

// ListDraftNotes gets a list of all draft notes for a merge request.
//
// Gitlab API docs:
// https://docs.gitlab.com/ee/api/draft_notes.html#list-all-merge-request-draft-notes
func (s *DraftNotesService) ListDraftNotes(pid interface{}, mergeRequest int, opt *ListDraftNotesOptions, options ...RequestOptionFunc) ([]*DraftNote, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes", PathEscape(project), mergeRequest)

req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
if err != nil {
return nil, nil, err
}

var n []*DraftNote
resp, err := s.client.Do(req, &n)
if err != nil {
return nil, resp, err
}

return n, resp, nil
}

// GetDraftNote gets a single draft note for a merge request.
//
// Gitlab API docs:
// https://docs.gitlab.com/ee/api/draft_notes.html#get-a-single-draft-note
func (s *DraftNotesService) GetDraftNote(pid interface{}, mergeRequest int, note int, options ...RequestOptionFunc) (*DraftNote, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes/%d", PathEscape(project), mergeRequest, note)

req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
if err != nil {
return nil, nil, err
}

n := new(DraftNote)
resp, err := s.client.Do(req, &n)
if err != nil {
return nil, resp, err
}

return n, resp, nil
}

// CreateDraftNoteOptions represents the available CreateDraftNote()
// options.
//
// Gitlab API docs:
// https://docs.gitlab.com/ee/api/draft_notes.html#create-a-draft-note
type CreateDraftNoteOptions struct {
Note *string `url:"note" json:"note"`
CommitID *string `url:"commit_id,omitempty" json:"commit_id,omitempty"`
InReplyToDiscussionID *string `url:"in_reply_to_discussion_id,omitempty" json:"in_reply_to_discussion_id,omitempty"`
ResolveDiscussion *bool `url:"resolve_discussion,omitempty" json:"resolve_discussion,omitempty"`
Position *PositionOptions `url:"position,omitempty" json:"position,omitempty"`
}

// CreateDraftNote creates a draft note for a merge request.
//
// Gitlab API docs:
// https://docs.gitlab.com/ee/api/draft_notes.html#create-a-draft-note
func (s *DraftNotesService) CreateDraftNote(pid interface{}, mergeRequest int, opt *CreateDraftNoteOptions, options ...RequestOptionFunc) (*DraftNote, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes", PathEscape(project), mergeRequest)

req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
if err != nil {
return nil, nil, err
}

n := new(DraftNote)
resp, err := s.client.Do(req, &n)
if err != nil {
return nil, resp, err
}

return n, resp, nil
}

// UpdateDraftNoteOptions represents the available UpdateDraftNote()
// options.
//
// Gitlab API docs:
// https://docs.gitlab.com/ee/api/draft_notes.html#create-a-draft-note
type UpdateDraftNoteOptions struct {
Note *string `url:"note,omitempty" json:"note,omitempty"`
Position *PositionOptions `url:"position,omitempty" json:"position,omitempty"`
}

// UpdateDraftNote updates a draft note for a merge request.
//
// Gitlab API docs: https://docs.gitlab.com/ee/api/draft_notes.html#create-a-draft-note
func (s *DraftNotesService) UpdateDraftNote(pid interface{}, mergeRequest int, note int, opt *UpdateDraftNoteOptions, options ...RequestOptionFunc) (*DraftNote, *Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, nil, err
}
u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes/%d", PathEscape(project), mergeRequest, note)

req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
if err != nil {
return nil, nil, err
}

n := new(DraftNote)
resp, err := s.client.Do(req, &n)
if err != nil {
return nil, resp, err
}

return n, resp, nil
}

// DeleteDraftNote deletes a single draft note for a merge request.
//
// Gitlab API docs:
// https://docs.gitlab.com/ee/api/draft_notes.html#delete-a-draft-note
func (s *DraftNotesService) DeleteDraftNote(pid interface{}, mergeRequest int, note int, options ...RequestOptionFunc) (*Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes/%d", PathEscape(project), mergeRequest, note)

req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
if err != nil {
return nil, err
}

return s.client.Do(req, nil)
}

// PublishDraftNote publishes a single draft note for a merge request.
//
// Gitlab API docs:
// https://docs.gitlab.com/ee/api/draft_notes.html#publish-a-draft-note
func (s *DraftNotesService) PublishDraftNote(pid interface{}, mergeRequest int, note int, options ...RequestOptionFunc) (*Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes/%d/publish", PathEscape(project), mergeRequest, note)

req, err := s.client.NewRequest(http.MethodPut, u, nil, options)
if err != nil {
return nil, err
}

return s.client.Do(req, nil)
}

// PublishAllDraftNotes publishes all draft notes for a merge request that belong to the user.
//
// Gitlab API docs:
// https://docs.gitlab.com/ee/api/draft_notes.html#publish-a-draft-note
func (s *DraftNotesService) PublishAllDraftNotes(pid interface{}, mergeRequest int, options ...RequestOptionFunc) (*Response, error) {
project, err := parseID(pid)
if err != nil {
return nil, err
}
u := fmt.Sprintf("projects/%s/merge_requests/%d/draft_notes/bulk_publish", PathEscape(project), mergeRequest)

req, err := s.client.NewRequest(http.MethodPost, u, nil, options)
if err != nil {
return nil, err
}

return s.client.Do(req, nil)
}
212 changes: 212 additions & 0 deletions draft_notes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
//
// 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 gitlab

import (
"net/http"
"reflect"
"testing"
)

func TestGetDraftNote(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes/3", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
mustWriteHTTPResponse(t, w, "testdata/get_draft_note.json")
})

note, _, err := client.DraftNotes.GetDraftNote("1", 4329, 3)
if err != nil {
t.Fatal(err)
}

want := &DraftNote{
ID: 37349978,
AuthorID: 10271899,
MergeRequestID: 291473309,
ResolveDiscussion: false,
DiscussionID: "",
Note: "Some draft note",
CommitID: "",
LineCode: "",
Position: nil,
}

if !reflect.DeepEqual(note, want) {
t.Errorf("DraftNotes.GetDraftNote want %#v, got %#v", note, want)
}
}

func TestListDraftNotes(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
mustWriteHTTPResponse(t, w, "testdata/list_draft_notes.json")
})

notes, _, err := client.DraftNotes.ListDraftNotes("1", 4329, nil)
if err != nil {
t.Fatal(err)
}

want := []*DraftNote{
{
ID: 37349978,
AuthorID: 10271899,
MergeRequestID: 291473309,
ResolveDiscussion: false,
DiscussionID: "",
Note: "Some draft note",
CommitID: "",
LineCode: "",
Position: nil,
},
{
ID: 37349979,
AuthorID: 10271899,
MergeRequestID: 291473309,
ResolveDiscussion: false,
DiscussionID: "",
Note: "Some draft note 2",
CommitID: "",
LineCode: "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
Position: &NotePosition{
BaseSHA: "64581c4ee41beb44d943d7801f82d9038e25e453",
StartSHA: "87bffbff93bf334889780f54ae1922355661f867",
HeadSHA: "2c972dbf9094c380f5f00dcd8112d2c69b24c859",
OldPath: "src/some-dir/some-file.js",
NewPath: "src/some-dir/some-file.js",
PositionType: "text",
NewLine: 9,
LineRange: &LineRange{
StartRange: &LinePosition{
LineCode: "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
Type: "new",
NewLine: 9,
},
EndRange: &LinePosition{
LineCode: "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
Type: "new",
NewLine: 9,
},
},
},
},
}

if !reflect.DeepEqual(notes, want) {
t.Errorf("DraftNotes.GetDraftNote want %#v, got %#v", notes, want)
}
}

func TestCreateDraftNote(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPost)
mustWriteHTTPResponse(t, w, "testdata/create_draft_note.json")
})

note, _, err := client.DraftNotes.CreateDraftNote("1", 4329, &CreateDraftNoteOptions{
Note: Ptr("Some new draft note"),
})
if err != nil {
t.Fatal(err)
}

want := &DraftNote{
ID: 37349980,
AuthorID: 10271899,
MergeRequestID: 291473309,
ResolveDiscussion: false,
DiscussionID: "",
Note: "Some new draft note",
CommitID: "",
LineCode: "",
Position: nil,
}

if !reflect.DeepEqual(note, want) {
t.Errorf("DraftNotes.GetDraftNote want %#v, got %#v", note, want)
}
}

func TestUpdateDraftNote(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes/3", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPut)
mustWriteHTTPResponse(t, w, "testdata/update_draft_note.json")
})

note, _, err := client.DraftNotes.UpdateDraftNote("1", 4329, 3, &UpdateDraftNoteOptions{
Note: Ptr("Some changed draft note"),
})
if err != nil {
t.Fatal(err)
}

want := &DraftNote{
ID: 37349980,
AuthorID: 10271899,
MergeRequestID: 291473309,
ResolveDiscussion: false,
DiscussionID: "",
Note: "Some changed draft note",
CommitID: "",
LineCode: "",
Position: nil,
}

if !reflect.DeepEqual(note, want) {
t.Errorf("DraftNotes.UpdateDraftNote want %#v, got %#v", note, want)
}
}

func TestDeleteDraftNote(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes/3", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodDelete)
})

_, err := client.DraftNotes.DeleteDraftNote("1", 4329, 3)
if err != nil {
t.Errorf("DraftNotes.DeleteDraftNote returned error: %v", err)
}
}

func TestPublishDraftNote(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes/3/publish", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPut)
})

_, err := client.DraftNotes.PublishDraftNote("1", 4329, 3)
if err != nil {
t.Errorf("DraftNotes.PublishDraftNote returned error: %v", err)
}
}

func TestPublishAllDraftNotes(t *testing.T) {
mux, client := setup(t)
mux.HandleFunc("/api/v4/projects/1/merge_requests/4329/draft_notes/bulk_publish", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPost)
})

_, err := client.DraftNotes.PublishAllDraftNotes("1", 4329)
if err != nil {
t.Errorf("DraftNotes.PublishAllDraftNotes returned error: %v", err)
}
}
2 changes: 1 addition & 1 deletion event_webhook_types.go
Original file line number Diff line number Diff line change
@@ -512,7 +512,7 @@ type MergeCommentEvent struct {
System bool `json:"system"`
Type string `json:"type"`
UpdatedAt string `json:"updated_at"`
UpdatedByID string `json:"updated_by_id"`
UpdatedByID int `json:"updated_by_id"`
Description string `json:"description"`
URL string `json:"url"`
} `json:"object_attributes"`
2 changes: 2 additions & 0 deletions gitlab.go
Original file line number Diff line number Diff line change
@@ -124,6 +124,7 @@ type Client struct {
Deployments *DeploymentsService
Discussions *DiscussionsService
DockerfileTemplate *DockerfileTemplatesService
DraftNotes *DraftNotesService
Environments *EnvironmentsService
EpicIssues *EpicIssuesService
Epics *EpicsService
@@ -357,6 +358,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
c.Deployments = &DeploymentsService{client: c}
c.Discussions = &DiscussionsService{client: c}
c.DockerfileTemplate = &DockerfileTemplatesService{client: c}
c.DraftNotes = &DraftNotesService{client: c}
c.Environments = &EnvironmentsService{client: c}
c.EpicIssues = &EpicIssuesService{client: c}
c.Epics = &EpicsService{client: c}
1 change: 1 addition & 0 deletions group_members.go
Original file line number Diff line number Diff line change
@@ -56,6 +56,7 @@ type GroupMember struct {
AccessLevel AccessLevelValue `json:"access_level"`
Email string `json:"email,omitempty"`
GroupSAMLIdentity *GroupMemberSAMLIdentity `json:"group_saml_identity"`
MemberRole *MemberRole `json:"member_role"`
}

// ListGroupMembersOptions represents the available ListGroupMembers() and
59 changes: 59 additions & 0 deletions group_members_test.go
Original file line number Diff line number Diff line change
@@ -287,3 +287,62 @@ func TestListGroupMembersWithSAML(t *testing.T) {
t.Errorf("Groups.ListBillableGroupMembers returned %+v, want %+v", members[0], want[0])
}
}

func TestGetGroupMemberCustomRole(t *testing.T) {
mux, client := setup(t)

path := fmt.Sprintf("/%sgroups/1/members/2", apiVersionPath)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)

// This is pulled straight from a `/group/<group_id>/members/<user_id>` call, then obfuscated.
fmt.Fprint(w, `
{
"id":1,
"username":"test",
"name":"testName",
"access_level":30,
"member_role":{
"id":1,
"group_id":2,
"name":"TestingCustomRole",
"description":"",
"base_access_level":30,
"admin_cicd_variables":true,
"admin_group_member":null,
"admin_merge_request":null,
"admin_push_rules":null,
"admin_terraform_state":null,
"admin_vulnerability":null,
"archive_project":null,
"manage_group_access_tokens":null,
"manage_project_access_tokens":null,
"read_code":null,
"read_dependency":null,
"read_vulnerability":null,
"remove_group":null,
"remove_project":null
}
}
`)
})

want := &GroupMember{
ID: 1,
Username: "test",
Name: "testName",
AccessLevel: AccessLevelValue(30),
MemberRole: &MemberRole{
ID: 1,
GroupId: 2,
Name: "TestingCustomRole",
Description: "",
BaseAccessLevel: AccessLevelValue(30),
AdminCICDVariables: true,
},
}
member, _, err := client.GroupMembers.GetGroupMember(1, 2)

assert.NoError(t, err)
assert.Equal(t, want, member)
}
5 changes: 3 additions & 2 deletions group_serviceaccounts.go
Original file line number Diff line number Diff line change
@@ -62,8 +62,9 @@ func (s *GroupsService) CreateServiceAccount(gid interface{}, options ...Request
// GitLab API docs:
// https://docs.gitlab.com/ee/api/groups.html#create-personal-access-token-for-service-account-user
type CreateServiceAccountPersonalAccessTokenOptions struct {
Scopes *[]string `url:"scopes,omitempty" json:"scopes,omitempty"`
Name *string `url:"name,omitempty" json:"name,omitempty"`
Scopes *[]string `url:"scopes,omitempty" json:"scopes,omitempty"`
Name *string `url:"name,omitempty" json:"name,omitempty"`
ExpiresAt *ISOTime `url:"expires_at,omitempty" json:"expires_at,omitempty"`
}

// CreateServiceAccountPersonalAccessToken add a new Personal Access Token for a
11 changes: 9 additions & 2 deletions group_serviceaccounts_test.go
Original file line number Diff line number Diff line change
@@ -22,6 +22,8 @@ import (
"reflect"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestCreateServiceAccount(t *testing.T) {
@@ -72,9 +74,14 @@ func TestCreateServiceAccountPersonalAccessToken(t *testing.T) {
"token":"random_token"
}`)
})

expireTime, err := ParseISOTime("2024-06-12")
require.NoError(t, err)

options := &CreateServiceAccountPersonalAccessTokenOptions{
Scopes: Ptr([]string{"api"}),
Name: Ptr("service_account_token"),
Scopes: Ptr([]string{"api"}),
Name: Ptr("service_account_token"),
ExpiresAt: Ptr(expireTime),
}
pat, _, err := client.Groups.CreateServiceAccountPersonalAccessToken(1, 57, options)
if err != nil {
15 changes: 12 additions & 3 deletions groups.go
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ type Group struct {
ProjectCreationLevel ProjectCreationLevelValue `json:"project_creation_level"`
AutoDevopsEnabled bool `json:"auto_devops_enabled"`
SubGroupCreationLevel SubGroupCreationLevelValue `json:"subgroup_creation_level"`
EmailsDisabled bool `json:"emails_disabled"`
EmailsEnabled bool `json:"emails_enabled"`
MentionsDisabled bool `json:"mentions_disabled"`
RunnersToken string `json:"runners_token"`
SharedProjects []*Project `json:"shared_projects"`
@@ -87,6 +87,9 @@ type Group struct {
CreatedAt *time.Time `json:"created_at"`
IPRestrictionRanges string `json:"ip_restriction_ranges"`
WikiAccessLevel AccessControlValue `json:"wiki_access_level"`

// Deprecated: Use EmailsEnabled instead
EmailsDisabled bool `json:"emails_disabled"`
}

// GroupAvatar represents a GitLab group avatar.
@@ -347,7 +350,7 @@ type CreateGroupOptions struct {
ProjectCreationLevel *ProjectCreationLevelValue `url:"project_creation_level,omitempty" json:"project_creation_level,omitempty"`
AutoDevopsEnabled *bool `url:"auto_devops_enabled,omitempty" json:"auto_devops_enabled,omitempty"`
SubGroupCreationLevel *SubGroupCreationLevelValue `url:"subgroup_creation_level,omitempty" json:"subgroup_creation_level,omitempty"`
EmailsDisabled *bool `url:"emails_disabled,omitempty" json:"emails_disabled,omitempty"`
EmailsEnabled *bool `url:"emails_enabled,omitempty" json:"emails_enabled,omitempty"`
MentionsDisabled *bool `url:"mentions_disabled,omitempty" json:"mentions_disabled,omitempty"`
LFSEnabled *bool `url:"lfs_enabled,omitempty" json:"lfs_enabled,omitempty"`
DefaultBranchProtection *int `url:"default_branch_protection,omitempty" json:"default_branch_protection"`
@@ -357,6 +360,9 @@ type CreateGroupOptions struct {
ExtraSharedRunnersMinutesLimit *int `url:"extra_shared_runners_minutes_limit,omitempty" json:"extra_shared_runners_minutes_limit,omitempty"`
IPRestrictionRanges *string `url:"ip_restriction_ranges,omitempty" json:"ip_restriction_ranges,omitempty"`
WikiAccessLevel *AccessControlValue `url:"wiki_access_level,omitempty" json:"wiki_access_level,omitempty"`

// Deprecated: Use EmailsEnabled instead
EmailsDisabled *bool `url:"emails_disabled,omitempty" json:"emails_disabled,omitempty"`
}

// CreateGroup creates a new project group. Available only for users who can
@@ -473,7 +479,7 @@ type UpdateGroupOptions struct {
ProjectCreationLevel *ProjectCreationLevelValue `url:"project_creation_level,omitempty" json:"project_creation_level,omitempty"`
AutoDevopsEnabled *bool `url:"auto_devops_enabled,omitempty" json:"auto_devops_enabled,omitempty"`
SubGroupCreationLevel *SubGroupCreationLevelValue `url:"subgroup_creation_level,omitempty" json:"subgroup_creation_level,omitempty"`
EmailsDisabled *bool `url:"emails_disabled,omitempty" json:"emails_disabled,omitempty"`
EmailsEnabled *bool `url:"emails_enabled,omitempty" json:"emails_enabled,omitempty"`
MentionsDisabled *bool `url:"mentions_disabled,omitempty" json:"mentions_disabled,omitempty"`
LFSEnabled *bool `url:"lfs_enabled,omitempty" json:"lfs_enabled,omitempty"`
RequestAccessEnabled *bool `url:"request_access_enabled,omitempty" json:"request_access_enabled,omitempty"`
@@ -486,6 +492,9 @@ type UpdateGroupOptions struct {
PreventSharingGroupsOutsideHierarchy *bool `url:"prevent_sharing_groups_outside_hierarchy,omitempty" json:"prevent_sharing_groups_outside_hierarchy,omitempty"`
IPRestrictionRanges *string `url:"ip_restriction_ranges,omitempty" json:"ip_restriction_ranges,omitempty"`
WikiAccessLevel *AccessControlValue `url:"wiki_access_level,omitempty" json:"wiki_access_level,omitempty"`

// Deprecated: Use EmailsEnabled instead
EmailsDisabled *bool `url:"emails_disabled,omitempty" json:"emails_disabled,omitempty"`
}

// UpdateGroup updates an existing group; only available to group owners and
115 changes: 114 additions & 1 deletion groups_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package gitlab

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"reflect"
@@ -179,7 +181,6 @@ func TestDeleteGroup_WithPermanentDelete(t *testing.T) {
PermanentlyRemove: Ptr(true),
FullPath: Ptr("testPath"),
})

if err != nil {
t.Errorf("Groups.DeleteGroup returned error: %v", err)
}
@@ -593,3 +594,115 @@ func TestUpdateGroupWithIPRestrictionRanges(t *testing.T) {
t.Errorf("Groups.UpdatedGroup returned %+v, want %+v", group, want)
}
}

func TestGetGroupWithEmailsEnabled(t *testing.T) {
mux, client := setup(t)

mux.HandleFunc("/api/v4/groups/1",
func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)

// Modified from https://docs.gitlab.com/ee/api/groups.html#details-of-a-group
fmt.Fprint(w, `
{
"id": 1,
"name": "test",
"path": "test",
"emails_enabled": true,
"description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
"visibility": "public",
"avatar_url": null,
"web_url": "https://gitlab.example.com/groups/test",
"request_access_enabled": false,
"repository_storage": "default",
"full_name": "test",
"full_path": "test",
"runners_token": "ba324ca7b1c77fc20bb9",
"file_template_project_id": 1,
"parent_id": null,
"enabled_git_access_protocol": "all",
"created_at": "2020-01-15T12:36:29.590Z",
"prevent_sharing_groups_outside_hierarchy": false,
"ip_restriction_ranges": null,
"math_rendering_limits_enabled": true,
"lock_math_rendering_limits_enabled": false
}`)
})

group, _, err := client.Groups.GetGroup(1, &GetGroupOptions{})
if err != nil {
t.Errorf("Groups.UpdateGroup returned error: %v", err)
}

if !group.EmailsEnabled {
t.Fatalf("Failed to parse `emails_enabled`. Wanted true, got %v", group.EmailsEnabled)
}
}

func TestCreateGroupWithEmailsEnabled(t *testing.T) {
mux, client := setup(t)

mux.HandleFunc("/api/v4/groups",
func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPost)

body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("Failed to read the request body. Error: %v", err)
}

// unmarshal into generic JSON since we don't want to test CreateGroupOptions using itself to validate.
var bodyJson map[string]interface{}
err = json.Unmarshal(body, &bodyJson)
if err != nil {
t.Fatalf("Failed to parse the request body into JSON. Error: %v", err)
}

if bodyJson["emails_enabled"] != true {
t.Fatalf("Test failed. `emails_enabled` expected to be true, got %v", bodyJson["emails_enabled"])
}

// Response is tested via the "GET" test, only test the actual request here.
fmt.Fprint(w, `
{}`)
})

_, _, err := client.Groups.CreateGroup(&CreateGroupOptions{EmailsEnabled: Ptr(true)})
if err != nil {
t.Errorf("Groups.CreateGroup returned error: %v", err)
}
}

func TestUpdateGroupWithEmailsEnabled(t *testing.T) {
mux, client := setup(t)

mux.HandleFunc("/api/v4/groups/1",
func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodPut)

body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("Failed to read the request body. Error: %v", err)
}

// unmarshal into generic JSON since we don't want to test UpdateGroupOptions using itself to validate.
var bodyJson map[string]interface{}
err = json.Unmarshal(body, &bodyJson)
if err != nil {
t.Fatalf("Failed to parse the request body into JSON. Error: %v", err)
}

if bodyJson["emails_enabled"] != true {
t.Fatalf("Test failed. `emails_enabled` expected to be true, got %v", bodyJson["emails_enabled"])
}

// Response is tested via the "GET" test, only test the actual request here.
fmt.Fprint(w, `
{}`)
})

_, _, err := client.Groups.UpdateGroup(1, &UpdateGroupOptions{EmailsEnabled: Ptr(true)})
if err != nil {
t.Errorf("Groups.UpdateGroup returned error: %v", err)
}
}
10 changes: 8 additions & 2 deletions member_roles.go
Original file line number Diff line number Diff line change
@@ -22,12 +22,18 @@ type MemberRole struct {
Description string `json:"description,omitempty"`
GroupId int `json:"group_id"`
BaseAccessLevel AccessLevelValue `json:"base_access_level"`
AdminMergeRequests bool `json:"admin_merge_requests,omitempty"`
AdminCICDVariables bool `json:"admin_cicd_variables,omitempty"`
AdminMergeRequests bool `json:"admin_merge_request,omitempty"`
AdminTerraformState bool `json:"admin_terraform_state,omitempty"`
AdminVulnerability bool `json:"admin_vulnerability,omitempty"`
ReadCode bool `json:"read_code,omitempty"`
ReadDependency bool `json:"read_dependency,omitempty"`
ReadVulnerability bool `json:"read_vulnerability,omitempty"`
ManageProjectAccessToken bool `json:"manage_project_access_token,omitempty"`
AdminGroupMembers bool `json:"admin_group_member,omitempty"`
ManageProjectAccessToken bool `json:"manage_project_access_tokens,omitempty"`
ArchiveProject bool `json:"archive_project,omitempty"`
RemoveProject bool `json:"remove_project,omitempty"`
ManageGroupAccesToken bool `json:"manage_group_access_tokens,omitempty"`
}

// ListMemberRoles gets a list of member roles for a specified group.
11 changes: 11 additions & 0 deletions testdata/create_draft_note.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": 37349980,
"author_id": 10271899,
"merge_request_id": 291473309,
"resolve_discussion": false,
"discussion_id": null,
"note": "Some new draft note",
"commit_id": null,
"position": null,
"line_code": null
}
11 changes: 11 additions & 0 deletions testdata/get_draft_note.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": 37349978,
"author_id": 10271899,
"merge_request_id": 291473309,
"resolve_discussion": false,
"discussion_id": null,
"note": "Some draft note",
"commit_id": null,
"position": null,
"line_code": null
}
47 changes: 47 additions & 0 deletions testdata/list_draft_notes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[
{
"id": 37349978,
"author_id": 10271899,
"merge_request_id": 291473309,
"resolve_discussion": false,
"discussion_id": null,
"note": "Some draft note",
"commit_id": null,
"position": null,
"line_code": null
},
{
"id": 37349979,
"author_id": 10271899,
"merge_request_id": 291473309,
"resolve_discussion": false,
"discussion_id": null,
"note": "Some draft note 2",
"commit_id": null,
"line_code": "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
"position": {
"base_sha": "64581c4ee41beb44d943d7801f82d9038e25e453",
"start_sha": "87bffbff93bf334889780f54ae1922355661f867",
"head_sha": "2c972dbf9094c380f5f00dcd8112d2c69b24c859",
"old_path": "src/some-dir/some-file.js",
"new_path": "src/some-dir/some-file.js",
"position_type": "text",
"old_line": null,
"new_line": 9,
"line_range": {
"start": {
"line_code": "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
"type": "new",
"old_line": null,
"new_line": 9
},
"end": {
"line_code": "3dacf79e0d779e2baa1c700cf56510e42f55cf85_10_9",
"type": "new",
"old_line": null,
"new_line": 9
}
}
}
}
]
11 changes: 11 additions & 0 deletions testdata/update_draft_note.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": 37349980,
"author_id": 10271899,
"merge_request_id": 291473309,
"resolve_discussion": false,
"discussion_id": null,
"note": "Some changed draft note",
"commit_id": null,
"position": null,
"line_code": null
}