1//
2// Copyright 2021, Sander van Harmelen
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16
17package gitlab
18
19import (
20	"fmt"
21	"net/http"
22	"time"
23)
24
25// MergeRequestApprovalsService handles communication with the merge request
26// approvals related methods of the GitLab API. This includes reading/updating
27// approval settings and approve/unapproving merge requests
28//
29// GitLab API docs: https://docs.gitlab.com/ee/api/merge_request_approvals.html
30type MergeRequestApprovalsService struct {
31	client *Client
32}
33
34// MergeRequestApprovals represents GitLab merge request approvals.
35//
36// GitLab API docs:
37// https://docs.gitlab.com/ee/api/merge_request_approvals.html#merge-request-level-mr-approvals
38type MergeRequestApprovals struct {
39	ID                             int                          `json:"id"`
40	IID                            int                          `json:"iid"`
41	ProjectID                      int                          `json:"project_id"`
42	Title                          string                       `json:"title"`
43	Description                    string                       `json:"description"`
44	State                          string                       `json:"state"`
45	CreatedAt                      *time.Time                   `json:"created_at"`
46	UpdatedAt                      *time.Time                   `json:"updated_at"`
47	MergeStatus                    string                       `json:"merge_status"`
48	Approved                       bool                         `json:"approved"`
49	ApprovalsBeforeMerge           int                          `json:"approvals_before_merge"`
50	ApprovalsRequired              int                          `json:"approvals_required"`
51	ApprovalsLeft                  int                          `json:"approvals_left"`
52	RequirePasswordToApprove       bool                         `json:"require_password_to_approve"`
53	ApprovedBy                     []*MergeRequestApproverUser  `json:"approved_by"`
54	SuggestedApprovers             []*BasicUser                 `json:"suggested_approvers"`
55	Approvers                      []*MergeRequestApproverUser  `json:"approvers"`
56	ApproverGroups                 []*MergeRequestApproverGroup `json:"approver_groups"`
57	UserHasApproved                bool                         `json:"user_has_approved"`
58	UserCanApprove                 bool                         `json:"user_can_approve"`
59	ApprovalRulesLeft              []*MergeRequestApprovalRule  `json:"approval_rules_left"`
60	HasApprovalRules               bool                         `json:"has_approval_rules"`
61	MergeRequestApproversAvailable bool                         `json:"merge_request_approvers_available"`
62	MultipleApprovalRulesAvailable bool                         `json:"multiple_approval_rules_available"`
63}
64
65func (m MergeRequestApprovals) String() string {
66	return Stringify(m)
67}
68
69// MergeRequestApproverGroup  represents GitLab project level merge request approver group.
70//
71// GitLab API docs:
72// https://docs.gitlab.com/ee/api/merge_request_approvals.html#project-level-mr-approvals
73type MergeRequestApproverGroup struct {
74	Group struct {
75		ID                   int    `json:"id"`
76		Name                 string `json:"name"`
77		Path                 string `json:"path"`
78		Description          string `json:"description"`
79		Visibility           string `json:"visibility"`
80		AvatarURL            string `json:"avatar_url"`
81		WebURL               string `json:"web_url"`
82		FullName             string `json:"full_name"`
83		FullPath             string `json:"full_path"`
84		LFSEnabled           bool   `json:"lfs_enabled"`
85		RequestAccessEnabled bool   `json:"request_access_enabled"`
86	}
87}
88
89// MergeRequestApprovalRule represents a GitLab merge request approval rule.
90//
91// GitLab API docs:
92// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-merge-request-level-rules
93type MergeRequestApprovalRule struct {
94	ID                   int                  `json:"id"`
95	Name                 string               `json:"name"`
96	RuleType             string               `json:"rule_type"`
97	EligibleApprovers    []*BasicUser         `json:"eligible_approvers"`
98	ApprovalsRequired    int                  `json:"approvals_required"`
99	SourceRule           *ProjectApprovalRule `json:"source_rule"`
100	Users                []*BasicUser         `json:"users"`
101	Groups               []*Group             `json:"groups"`
102	ContainsHiddenGroups bool                 `json:"contains_hidden_groups"`
103	Section              string               `json:"section"`
104	ApprovedBy           []*BasicUser         `json:"approved_by"`
105	Approved             bool                 `json:"approved"`
106}
107
108// MergeRequestApprovalState represents a GitLab merge request approval state.
109//
110// GitLab API docs:
111// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-the-approval-state-of-merge-requests
112type MergeRequestApprovalState struct {
113	ApprovalRulesOverwritten bool                        `json:"approval_rules_overwritten"`
114	Rules                    []*MergeRequestApprovalRule `json:"rules"`
115}
116
117// String is a stringify for MergeRequestApprovalRule
118func (s MergeRequestApprovalRule) String() string {
119	return Stringify(s)
120}
121
122// MergeRequestApproverUser  represents GitLab project level merge request approver user.
123//
124// GitLab API docs:
125// https://docs.gitlab.com/ee/api/merge_request_approvals.html#project-level-mr-approvals
126type MergeRequestApproverUser struct {
127	User *BasicUser
128}
129
130// ApproveMergeRequestOptions represents the available ApproveMergeRequest() options.
131//
132// GitLab API docs:
133// https://docs.gitlab.com/ee/api/merge_request_approvals.html#approve-merge-request
134type ApproveMergeRequestOptions struct {
135	SHA *string `url:"sha,omitempty" json:"sha,omitempty"`
136}
137
138// ApproveMergeRequest approves a merge request on GitLab. If a non-empty sha
139// is provided then it must match the sha at the HEAD of the MR.
140//
141// GitLab API docs:
142// https://docs.gitlab.com/ee/api/merge_request_approvals.html#approve-merge-request
143func (s *MergeRequestApprovalsService) ApproveMergeRequest(pid interface{}, mr int, opt *ApproveMergeRequestOptions, options ...RequestOptionFunc) (*MergeRequestApprovals, *Response, error) {
144	project, err := parseID(pid)
145	if err != nil {
146		return nil, nil, err
147	}
148	u := fmt.Sprintf("projects/%s/merge_requests/%d/approve", PathEscape(project), mr)
149
150	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
151	if err != nil {
152		return nil, nil, err
153	}
154
155	m := new(MergeRequestApprovals)
156	resp, err := s.client.Do(req, m)
157	if err != nil {
158		return nil, resp, err
159	}
160
161	return m, resp, err
162}
163
164// UnapproveMergeRequest unapproves a previously approved merge request on GitLab.
165//
166// GitLab API docs:
167// https://docs.gitlab.com/ee/api/merge_request_approvals.html#unapprove-merge-request
168func (s *MergeRequestApprovalsService) UnapproveMergeRequest(pid interface{}, mr int, options ...RequestOptionFunc) (*Response, error) {
169	project, err := parseID(pid)
170	if err != nil {
171		return nil, err
172	}
173	u := fmt.Sprintf("projects/%s/merge_requests/%d/unapprove", PathEscape(project), mr)
174
175	req, err := s.client.NewRequest(http.MethodPost, u, nil, options)
176	if err != nil {
177		return nil, err
178	}
179
180	return s.client.Do(req, nil)
181}
182
183// ChangeMergeRequestApprovalConfigurationOptions represents the available
184// ChangeMergeRequestApprovalConfiguration() options.
185//
186// GitLab API docs:
187// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-approval-configuration
188type ChangeMergeRequestApprovalConfigurationOptions struct {
189	ApprovalsRequired *int `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
190}
191
192// GetConfiguration shows information about single merge request approvals
193//
194// GitLab API docs:
195// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-configuration-1
196func (s *MergeRequestApprovalsService) GetConfiguration(pid interface{}, mr int, options ...RequestOptionFunc) (*MergeRequestApprovals, *Response, error) {
197	project, err := parseID(pid)
198	if err != nil {
199		return nil, nil, err
200	}
201	u := fmt.Sprintf("projects/%s/merge_requests/%d/approvals", PathEscape(project), mr)
202
203	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
204	if err != nil {
205		return nil, nil, err
206	}
207
208	m := new(MergeRequestApprovals)
209	resp, err := s.client.Do(req, m)
210	if err != nil {
211		return nil, resp, err
212	}
213
214	return m, resp, err
215}
216
217// ChangeApprovalConfiguration updates the approval configuration of a merge request.
218//
219// GitLab API docs:
220// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-approval-configuration
221func (s *MergeRequestApprovalsService) ChangeApprovalConfiguration(pid interface{}, mergeRequest int, opt *ChangeMergeRequestApprovalConfigurationOptions, options ...RequestOptionFunc) (*MergeRequest, *Response, error) {
222	project, err := parseID(pid)
223	if err != nil {
224		return nil, nil, err
225	}
226	u := fmt.Sprintf("projects/%s/merge_requests/%d/approvals", PathEscape(project), mergeRequest)
227
228	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
229	if err != nil {
230		return nil, nil, err
231	}
232
233	m := new(MergeRequest)
234	resp, err := s.client.Do(req, m)
235	if err != nil {
236		return nil, resp, err
237	}
238
239	return m, resp, err
240}
241
242// ChangeMergeRequestAllowedApproversOptions represents the available
243// ChangeMergeRequestAllowedApprovers() options.
244//
245// GitLab API docs:
246// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-allowed-approvers-for-merge-request
247type ChangeMergeRequestAllowedApproversOptions struct {
248	ApproverIDs      []int `url:"approver_ids" json:"approver_ids"`
249	ApproverGroupIDs []int `url:"approver_group_ids" json:"approver_group_ids"`
250}
251
252// ChangeAllowedApprovers updates the approvers for a merge request.
253//
254// GitLab API docs:
255// https://docs.gitlab.com/ee/api/merge_request_approvals.html#change-allowed-approvers-for-merge-request
256func (s *MergeRequestApprovalsService) ChangeAllowedApprovers(pid interface{}, mergeRequest int, opt *ChangeMergeRequestAllowedApproversOptions, options ...RequestOptionFunc) (*MergeRequest, *Response, error) {
257	project, err := parseID(pid)
258	if err != nil {
259		return nil, nil, err
260	}
261	u := fmt.Sprintf("projects/%s/merge_requests/%d/approvers", PathEscape(project), mergeRequest)
262
263	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
264	if err != nil {
265		return nil, nil, err
266	}
267
268	m := new(MergeRequest)
269	resp, err := s.client.Do(req, m)
270	if err != nil {
271		return nil, resp, err
272	}
273
274	return m, resp, err
275}
276
277// GetApprovalRules requests information about a merge request’s approval rules
278//
279// GitLab API docs:
280// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-merge-request-level-rules
281func (s *MergeRequestApprovalsService) GetApprovalRules(pid interface{}, mergeRequest int, options ...RequestOptionFunc) ([]*MergeRequestApprovalRule, *Response, error) {
282	project, err := parseID(pid)
283	if err != nil {
284		return nil, nil, err
285	}
286	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules", PathEscape(project), mergeRequest)
287
288	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
289	if err != nil {
290		return nil, nil, err
291	}
292
293	var par []*MergeRequestApprovalRule
294	resp, err := s.client.Do(req, &par)
295	if err != nil {
296		return nil, resp, err
297	}
298
299	return par, resp, err
300}
301
302// GetApprovalState requests information about a merge request’s approval state
303//
304// GitLab API docs:
305// https://docs.gitlab.com/ee/api/merge_request_approvals.html#get-the-approval-state-of-merge-requests
306func (s *MergeRequestApprovalsService) GetApprovalState(pid interface{}, mergeRequest int, options ...RequestOptionFunc) (*MergeRequestApprovalState, *Response, error) {
307	project, err := parseID(pid)
308	if err != nil {
309		return nil, nil, err
310	}
311	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_state", PathEscape(project), mergeRequest)
312
313	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
314	if err != nil {
315		return nil, nil, err
316	}
317
318	var pas *MergeRequestApprovalState
319	resp, err := s.client.Do(req, &pas)
320	if err != nil {
321		return nil, resp, err
322	}
323
324	return pas, resp, err
325}
326
327// CreateMergeRequestApprovalRuleOptions represents the available CreateApprovalRule()
328// options.
329//
330// GitLab API docs:
331// https://docs.gitlab.com/ee/api/merge_request_approvals.html#create-merge-request-level-rule
332type CreateMergeRequestApprovalRuleOptions struct {
333	Name                  *string `url:"name,omitempty" json:"name,omitempty"`
334	ApprovalsRequired     *int    `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
335	ApprovalProjectRuleID *int    `url:"approval_project_rule_id,omitempty" json:"approval_project_rule_id,omitempty"`
336	UserIDs               *[]int  `url:"user_ids,omitempty" json:"user_ids,omitempty"`
337	GroupIDs              *[]int  `url:"group_ids,omitempty" json:"group_ids,omitempty"`
338}
339
340// CreateApprovalRule creates a new MR level approval rule.
341//
342// GitLab API docs:
343// https://docs.gitlab.com/ee/api/merge_request_approvals.html#create-merge-request-level-rule
344func (s *MergeRequestApprovalsService) CreateApprovalRule(pid interface{}, mergeRequest int, opt *CreateMergeRequestApprovalRuleOptions, options ...RequestOptionFunc) (*MergeRequestApprovalRule, *Response, error) {
345	project, err := parseID(pid)
346	if err != nil {
347		return nil, nil, err
348	}
349	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules", PathEscape(project), mergeRequest)
350
351	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
352	if err != nil {
353		return nil, nil, err
354	}
355
356	par := new(MergeRequestApprovalRule)
357	resp, err := s.client.Do(req, &par)
358	if err != nil {
359		return nil, resp, err
360	}
361
362	return par, resp, err
363}
364
365// UpdateMergeRequestApprovalRuleOptions represents the available UpdateApprovalRule()
366// options.
367//
368// GitLab API docs:
369// https://docs.gitlab.com/ee/api/merge_request_approvals.html#update-merge-request-level-rule
370type UpdateMergeRequestApprovalRuleOptions struct {
371	Name              *string `url:"name,omitempty" json:"name,omitempty"`
372	ApprovalsRequired *int    `url:"approvals_required,omitempty" json:"approvals_required,omitempty"`
373	UserIDs           *[]int  `url:"user_ids,omitempty" json:"user_ids,omitempty"`
374	GroupIDs          *[]int  `url:"group_ids,omitempty" json:"group_ids,omitempty"`
375}
376
377// UpdateApprovalRule updates an existing approval rule with new options.
378//
379// GitLab API docs:
380// https://docs.gitlab.com/ee/api/merge_request_approvals.html#update-merge-request-level-rule
381func (s *MergeRequestApprovalsService) UpdateApprovalRule(pid interface{}, mergeRequest int, approvalRule int, opt *UpdateMergeRequestApprovalRuleOptions, options ...RequestOptionFunc) (*MergeRequestApprovalRule, *Response, error) {
382	project, err := parseID(pid)
383	if err != nil {
384		return nil, nil, err
385	}
386	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules/%d", PathEscape(project), mergeRequest, approvalRule)
387
388	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
389	if err != nil {
390		return nil, nil, err
391	}
392
393	par := new(MergeRequestApprovalRule)
394	resp, err := s.client.Do(req, &par)
395	if err != nil {
396		return nil, resp, err
397	}
398
399	return par, resp, err
400}
401
402// DeleteApprovalRule deletes a mr level approval rule.
403//
404// GitLab API docs:
405// https://docs.gitlab.com/ee/api/merge_request_approvals.html#delete-merge-request-level-rule
406func (s *MergeRequestApprovalsService) DeleteApprovalRule(pid interface{}, mergeRequest int, approvalRule int, options ...RequestOptionFunc) (*Response, error) {
407	project, err := parseID(pid)
408	if err != nil {
409		return nil, err
410	}
411	u := fmt.Sprintf("projects/%s/merge_requests/%d/approval_rules/%d", PathEscape(project), mergeRequest, approvalRule)
412
413	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
414	if err != nil {
415		return nil, err
416	}
417
418	return s.client.Do(req, nil)
419}
420