Repository: xanzy/go-gitlab
Pull request #717 (2019-10-18 02:31:48)

Adds support for project-level approval rules
Closes https://github.com/xanzy/go-gitlab/issues/700


Patch:
diff --git a/projects.go b/projects.go
index 90a1307..47cbae8 100644
--- a/projects.go
+++ b/projects.go
@@ -184,6 +184,25 @@ func (s Project) String() string {
 	return Stringify(s)
 }
 
+// ProjectApprovalRule represents a GitLab project approval rule.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-project-level-rules
+type ProjectApprovalRule struct {
+	ID                   int          `json:"id"`
+	Name                 string       `json:"name"`
+	RuleType             string       `json:"rule_type"`
+	EligibleApprovers    []*BasicUser `json:"eligible_approvers"`
+	ApprovalsRequired    int          `json:"approvals_required"`
+	Users                []*BasicUser `json:"users"`
+	Groups               []*Group     `json:"groups"`
+	ContainsHiddenGroups bool         `json:"contains_hidden_groups"`
+}
+
+func (s ProjectApprovalRule) String() string {
+	return Stringify(s)
+}
+
 // ListProjectsOptions represents the available ListProjects() options.
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#list-projects
@@ -1313,14 +1332,132 @@ func (s *ProjectsService) ChangeApprovalConfiguration(pid interface{}, opt *Chan
 	return pa, resp, err
 }
 
+// GetProjectApprovalRules looks up the list of project level approvers.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-project-level-rules
+func (s *ProjectsService) GetProjectApprovalRules(pid interface{}, options ...OptionFunc) ([]*ProjectApprovalRule, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/approval_rules", pathEscape(project))
+
+	req, err := s.client.NewRequest("GET", u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var par []*ProjectApprovalRule
+	resp, err := s.client.Do(req, &par)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return par, resp, err
+}
+
+// CreateProjectLevelRuleOptions represents the available CreateProjectApprovalRule()
+// options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/merge_request_approvals.html#create-project-level-rules
+type CreateProjectLevelRuleOptions struct {
+	Name              *string `url:"name,omitempty" json:"name,omitempty"`
+	ApprovalsRequired *int    `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
+	UserIDs           []int   `url:"user_ids,omitempty" json:"user_ids,omitempty"`
+	GroupIDs          []int   `url:"group_ids,omitempty" json:"group_ids,omitempty"`
+}
+
+// CreateProjectApprovalRule creates a new project-level approval rule.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/merge_request_approvals.html#create-project-level-rules
+func (s *ProjectsService) CreateProjectApprovalRule(pid interface{}, opt *CreateProjectLevelRuleOptions, options ...OptionFunc) (*ProjectApprovalRule, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/approval_rules", pathEscape(project))
+
+	req, err := s.client.NewRequest("POST", u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	par := new(ProjectApprovalRule)
+	resp, err := s.client.Do(req, &par)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return par, resp, err
+}
+
+// UpdateProjectLevelRuleOptions represents the available UpdateProjectApprovalRule()
+// options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/merge_request_approvals.html#update-project-level-rules
+type UpdateProjectLevelRuleOptions struct {
+	Name              *string `url:"name,omitempty" json:"name,omitempty"`
+	ApprovalsRequired *int    `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
+	UserIDs           []int   `url:"user_ids,omitempty" json:"user_ids,omitempty"`
+	GroupIDs          []int   `url:"group_ids,omitempty" json:"group_ids,omitempty"`
+}
+
+// UpdateProjectApprovalRule updates an existing approval rule with new options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/merge_request_approvals.html#update-project-level-rules
+func (s *ProjectsService) UpdateProjectApprovalRule(pid interface{}, approvalRule int, opt *UpdateProjectLevelRuleOptions, options ...OptionFunc) (*ProjectApprovalRule, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/approval_rules/%d", pathEscape(project), approvalRule)
+
+	req, err := s.client.NewRequest("PUT", u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	par := new(ProjectApprovalRule)
+	resp, err := s.client.Do(req, &par)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return par, resp, err
+}
+
+// DeleteProjectApprovalRule deletes a project-level approval rule.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/merge_request_approvals.html#delete-project-level-rules
+func (s *ProjectsService) DeleteProjectApprovalRule(pid interface{}, approvalRule int, options ...OptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/approval_rules/%d", pathEscape(project), approvalRule)
+
+	req, err := s.client.NewRequest("DELETE", u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
 // 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" json:"approver_ids"`
-	ApproverGroupIDs []*int `url:"approver_group_ids" json:"approver_group_ids"`
+	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.


Test patch:
diff --git a/projects_test.go b/projects_test.go
index e354aff..5663a72 100644
--- a/projects_test.go
+++ b/projects_test.go
@@ -477,8 +477,8 @@ func TestChangeAllowedApprovers(t *testing.T) {
 	})
 
 	opt := &ChangeAllowedApproversOptions{
-		ApproverIDs:      []*int{Int(1)},
-		ApproverGroupIDs: []*int{Int(2)},
+		ApproverIDs:      []int{1},
+		ApproverGroupIDs: []int{2},
 	}
 
 	approvals, _, err := client.Projects.ChangeAllowedApprovers(1, opt)
@@ -548,3 +548,256 @@ func TestForkProject(t *testing.T) {
 		t.Errorf("Projects.ForProject returned %+v, want %+v", project, want)
 	}
 }
+
+func TestGetProjectApprovalRules(t *testing.T) {
+	mux, server, client := setup()
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/approval_rules", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `[
+			{
+				"id": 1,
+				"name": "security",
+				"rule_type": "regular",
+				"eligible_approvers": [
+					{
+						"id": 5,
+						"name": "John Doe",
+						"username": "jdoe",
+						"state": "active",
+						"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+						"web_url": "http://localhost/jdoe"
+					},
+					{
+						"id": 50,
+						"name": "Group Member 1",
+						"username": "group_member_1",
+						"state": "active",
+						"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+						"web_url": "http://localhost/group_member_1"
+					}
+				],
+				"approvals_required": 3,
+				"users": [
+					{
+						"id": 5,
+						"name": "John Doe",
+						"username": "jdoe",
+						"state": "active",
+						"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+						"web_url": "http://localhost/jdoe"
+					}
+				],
+				"groups": [
+					{
+						"id": 5,
+						"name": "group1",
+						"path": "group1",
+						"description": "",
+						"visibility": "public",
+						"lfs_enabled": false,
+						"avatar_url": null,
+						"web_url": "http://localhost/groups/group1",
+						"request_access_enabled": false,
+						"full_name": "group1",
+						"full_path": "group1",
+						"parent_id": null,
+						"ldap_cn": null,
+						"ldap_access": null
+					}
+				],
+				"contains_hidden_groups": false
+			}
+		]`)
+	})
+
+	approvals, _, err := client.Projects.GetProjectApprovalRules(1)
+	if err != nil {
+		t.Errorf("Projects.GetProjectApprovalRules returned error: %v", err)
+	}
+
+	want := []*ProjectApprovalRule{
+		&ProjectApprovalRule{
+			ID:       1,
+			Name:     "security",
+			RuleType: "regular",
+			EligibleApprovers: []*BasicUser{
+				&BasicUser{
+					ID:        5,
+					Name:      "John Doe",
+					Username:  "jdoe",
+					State:     "active",
+					AvatarURL: "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+					WebURL:    "http://localhost/jdoe",
+				},
+				&BasicUser{
+					ID:        50,
+					Name:      "Group Member 1",
+					Username:  "group_member_1",
+					State:     "active",
+					AvatarURL: "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+					WebURL:    "http://localhost/group_member_1",
+				},
+			},
+			ApprovalsRequired: 3,
+			Users: []*BasicUser{
+				&BasicUser{
+					ID:        5,
+					Name:      "John Doe",
+					Username:  "jdoe",
+					State:     "active",
+					AvatarURL: "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+					WebURL:    "http://localhost/jdoe",
+				},
+			},
+			Groups: []*Group{
+				&Group{
+					ID:                   5,
+					Name:                 "group1",
+					Path:                 "group1",
+					Description:          "",
+					Visibility:           Visibility(PublicVisibility),
+					LFSEnabled:           false,
+					AvatarURL:            "",
+					WebURL:               "http://localhost/groups/group1",
+					RequestAccessEnabled: false,
+					FullName:             "group1",
+					FullPath:             "group1",
+				},
+			},
+		},
+	}
+
+	if !reflect.DeepEqual(want, approvals) {
+		t.Errorf("Projects.GetProjectApprovalRules returned %+v, want %+v", approvals, want)
+	}
+}
+
+func TestCreateProjectApprovalRule(t *testing.T) {
+	mux, server, client := setup()
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/approval_rules", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		fmt.Fprint(w, `{
+			"id": 1,
+			"name": "security",
+			"rule_type": "regular",
+			"eligible_approvers": [
+				{
+					"id": 5,
+					"name": "John Doe",
+					"username": "jdoe",
+					"state": "active",
+					"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+					"web_url": "http://localhost/jdoe"
+				},
+				{
+					"id": 50,
+					"name": "Group Member 1",
+					"username": "group_member_1",
+					"state": "active",
+					"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+					"web_url": "http://localhost/group_member_1"
+				}
+			],
+			"approvals_required": 3,
+			"users": [
+				{
+					"id": 5,
+					"name": "John Doe",
+					"username": "jdoe",
+					"state": "active",
+					"avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+					"web_url": "http://localhost/jdoe"
+				}
+			],
+			"groups": [
+				{
+					"id": 5,
+					"name": "group1",
+					"path": "group1",
+					"description": "",
+					"visibility": "public",
+					"lfs_enabled": false,
+					"avatar_url": null,
+					"web_url": "http://localhost/groups/group1",
+					"request_access_enabled": false,
+					"full_name": "group1",
+					"full_path": "group1",
+					"parent_id": null,
+					"ldap_cn": null,
+					"ldap_access": null
+				}
+			],
+			"contains_hidden_groups": false
+		}`)
+	})
+
+	opt := &CreateProjectLevelRuleOptions{
+		Name:              String("security"),
+		ApprovalsRequired: Int(3),
+		UserIDs:           []int{5, 50},
+		GroupIDs:          []int{5},
+	}
+
+	rule, _, err := client.Projects.CreateProjectApprovalRule(1, opt)
+	if err != nil {
+		t.Errorf("Projects.CreateProjectApprovalRule returned error: %v", err)
+	}
+
+	want := &ProjectApprovalRule{
+		ID:       1,
+		Name:     "security",
+		RuleType: "regular",
+		EligibleApprovers: []*BasicUser{
+			&BasicUser{
+				ID:        5,
+				Name:      "John Doe",
+				Username:  "jdoe",
+				State:     "active",
+				AvatarURL: "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+				WebURL:    "http://localhost/jdoe",
+			},
+			&BasicUser{
+				ID:        50,
+				Name:      "Group Member 1",
+				Username:  "group_member_1",
+				State:     "active",
+				AvatarURL: "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+				WebURL:    "http://localhost/group_member_1",
+			},
+		},
+		ApprovalsRequired: 3,
+		Users: []*BasicUser{
+			&BasicUser{
+				ID:        5,
+				Name:      "John Doe",
+				Username:  "jdoe",
+				State:     "active",
+				AvatarURL: "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+				WebURL:    "http://localhost/jdoe",
+			},
+		},
+		Groups: []*Group{
+			&Group{
+				ID:                   5,
+				Name:                 "group1",
+				Path:                 "group1",
+				Description:          "",
+				Visibility:           Visibility(PublicVisibility),
+				LFSEnabled:           false,
+				AvatarURL:            "",
+				WebURL:               "http://localhost/groups/group1",
+				RequestAccessEnabled: false,
+				FullName:             "group1",
+				FullPath:             "group1",
+			},
+		},
+	}
+
+	if !reflect.DeepEqual(want, rule) {
+		t.Errorf("Projects.CreateProjectApprovalRule returned %+v, want %+v", rule, want)
+	}
+}