1//
2// Copyright 2017, 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	"encoding/json"
21	"fmt"
22	"strings"
23	"time"
24)
25
26// IssuesService handles communication with the issue related methods
27// of the GitLab API.
28//
29// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html
30type IssuesService struct {
31	client    *Client
32	timeStats *timeStatsService
33}
34
35// IssueAuthor represents a author of the issue.
36type IssueAuthor struct {
37	ID        int    `json:"id"`
38	State     string `json:"state"`
39	WebURL    string `json:"web_url"`
40	Name      string `json:"name"`
41	AvatarURL string `json:"avatar_url"`
42	Username  string `json:"username"`
43}
44
45// IssueAssignee represents a assignee of the issue.
46type IssueAssignee struct {
47	ID        int    `json:"id"`
48	State     string `json:"state"`
49	WebURL    string `json:"web_url"`
50	Name      string `json:"name"`
51	AvatarURL string `json:"avatar_url"`
52	Username  string `json:"username"`
53}
54
55// IssueLinks represents links of the issue.
56type IssueLinks struct {
57	Self       string `json:"self"`
58	Notes      string `json:"notes"`
59	AwardEmoji string `json:"award_emoji"`
60	Project    string `json:"project"`
61}
62
63// Issue represents a GitLab issue.
64//
65// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html
66type Issue struct {
67	ID                int              `json:"id"`
68	IID               int              `json:"iid"`
69	ProjectID         int              `json:"project_id"`
70	Milestone         *Milestone       `json:"milestone"`
71	Author            *IssueAuthor     `json:"author"`
72	Description       string           `json:"description"`
73	State             string           `json:"state"`
74	Assignees         []*IssueAssignee `json:"assignees"`
75	Assignee          *IssueAssignee   `json:"assignee"`
76	Upvotes           int              `json:"upvotes"`
77	Downvotes         int              `json:"downvotes"`
78	Labels            []string         `json:"labels"`
79	Title             string           `json:"title"`
80	UpdatedAt         *time.Time       `json:"updated_at"`
81	CreatedAt         *time.Time       `json:"created_at"`
82	ClosedAt          *time.Time       `json:"closed_at"`
83	Subscribed        bool             `json:"subscribed"`
84	UserNotesCount    int              `json:"user_notes_count"`
85	DueDate           *ISOTime         `json:"due_date"`
86	WebURL            string           `json:"web_url"`
87	TimeStats         *TimeStats       `json:"time_stats"`
88	Confidential      bool             `json:"confidential"`
89	Weight            int              `json:"weight"`
90	DiscussionLocked  bool             `json:"discussion_locked"`
91	Links             *IssueLinks      `json:"_links"`
92	IssueLinkID       int              `json:"issue_link_id"`
93	MergeRequestCount int              `json:"merge_requests_count"`
94}
95
96func (i Issue) String() string {
97	return Stringify(i)
98}
99
100// Labels is a custom type with specific marshaling characteristics.
101type Labels []string
102
103// MarshalJSON implements the json.Marshaler interface.
104func (l *Labels) MarshalJSON() ([]byte, error) {
105	return json.Marshal(strings.Join(*l, ","))
106}
107
108// ListIssuesOptions represents the available ListIssues() options.
109//
110// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-issues
111type ListIssuesOptions struct {
112	ListOptions
113	State           *string    `url:"state,omitempty" json:"state,omitempty"`
114	Labels          Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
115	Milestone       *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
116	Scope           *string    `url:"scope,omitempty" json:"scope,omitempty"`
117	AuthorID        *int       `url:"author_id,omitempty" json:"author_id,omitempty"`
118	AssigneeID      *int       `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
119	MyReactionEmoji *string    `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
120	IIDs            []int      `url:"iids[],omitempty" json:"iids,omitempty"`
121	OrderBy         *string    `url:"order_by,omitempty" json:"order_by,omitempty"`
122	Sort            *string    `url:"sort,omitempty" json:"sort,omitempty"`
123	Search          *string    `url:"search,omitempty" json:"search,omitempty"`
124	CreatedAfter    *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
125	CreatedBefore   *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
126	UpdatedAfter    *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
127	UpdatedBefore   *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"`
128}
129
130// ListIssues gets all issues created by authenticated user. This function
131// takes pagination parameters page and per_page to restrict the list of issues.
132//
133// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-issues
134func (s *IssuesService) ListIssues(opt *ListIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) {
135	req, err := s.client.NewRequest("GET", "issues", opt, options)
136	if err != nil {
137		return nil, nil, err
138	}
139
140	var i []*Issue
141	resp, err := s.client.Do(req, &i)
142	if err != nil {
143		return nil, resp, err
144	}
145
146	return i, resp, err
147}
148
149// ListGroupIssuesOptions represents the available ListGroupIssues() options.
150//
151// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-group-issues
152type ListGroupIssuesOptions struct {
153	ListOptions
154	State           *string    `url:"state,omitempty" json:"state,omitempty"`
155	Labels          Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
156	IIDs            []int      `url:"iids[],omitempty" json:"iids,omitempty"`
157	Milestone       *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
158	Scope           *string    `url:"scope,omitempty" json:"scope,omitempty"`
159	AuthorID        *int       `url:"author_id,omitempty" json:"author_id,omitempty"`
160	AssigneeID      *int       `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
161	MyReactionEmoji *string    `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
162	OrderBy         *string    `url:"order_by,omitempty" json:"order_by,omitempty"`
163	Sort            *string    `url:"sort,omitempty" json:"sort,omitempty"`
164	Search          *string    `url:"search,omitempty" json:"search,omitempty"`
165	CreatedAfter    *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
166	CreatedBefore   *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
167	UpdatedAfter    *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
168	UpdatedBefore   *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"`
169}
170
171// ListGroupIssues gets a list of group issues. This function accepts
172// pagination parameters page and per_page to return the list of group issues.
173//
174// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-group-issues
175func (s *IssuesService) ListGroupIssues(pid interface{}, opt *ListGroupIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) {
176	group, err := parseID(pid)
177	if err != nil {
178		return nil, nil, err
179	}
180	u := fmt.Sprintf("groups/%s/issues", pathEscape(group))
181
182	req, err := s.client.NewRequest("GET", u, opt, options)
183	if err != nil {
184		return nil, nil, err
185	}
186
187	var i []*Issue
188	resp, err := s.client.Do(req, &i)
189	if err != nil {
190		return nil, resp, err
191	}
192
193	return i, resp, err
194}
195
196// ListProjectIssuesOptions represents the available ListProjectIssues() options.
197//
198// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-project-issues
199type ListProjectIssuesOptions struct {
200	ListOptions
201	IIDs            []int      `url:"iids[],omitempty" json:"iids,omitempty"`
202	State           *string    `url:"state,omitempty" json:"state,omitempty"`
203	Labels          Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
204	Milestone       *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
205	Scope           *string    `url:"scope,omitempty" json:"scope,omitempty"`
206	AuthorID        *int       `url:"author_id,omitempty" json:"author_id,omitempty"`
207	AssigneeID      *int       `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
208	MyReactionEmoji *string    `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
209	OrderBy         *string    `url:"order_by,omitempty" json:"order_by,omitempty"`
210	Sort            *string    `url:"sort,omitempty" json:"sort,omitempty"`
211	Search          *string    `url:"search,omitempty" json:"search,omitempty"`
212	CreatedAfter    *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
213	CreatedBefore   *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
214	UpdatedAfter    *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
215	UpdatedBefore   *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"`
216}
217
218// ListProjectIssues gets a list of project issues. This function accepts
219// pagination parameters page and per_page to return the list of project issues.
220//
221// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-project-issues
222func (s *IssuesService) ListProjectIssues(pid interface{}, opt *ListProjectIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) {
223	project, err := parseID(pid)
224	if err != nil {
225		return nil, nil, err
226	}
227	u := fmt.Sprintf("projects/%s/issues", pathEscape(project))
228
229	req, err := s.client.NewRequest("GET", u, opt, options)
230	if err != nil {
231		return nil, nil, err
232	}
233
234	var i []*Issue
235	resp, err := s.client.Do(req, &i)
236	if err != nil {
237		return nil, resp, err
238	}
239
240	return i, resp, err
241}
242
243// GetIssue gets a single project issue.
244//
245// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#single-issues
246func (s *IssuesService) GetIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) {
247	project, err := parseID(pid)
248	if err != nil {
249		return nil, nil, err
250	}
251	u := fmt.Sprintf("projects/%s/issues/%d", pathEscape(project), issue)
252
253	req, err := s.client.NewRequest("GET", u, nil, options)
254	if err != nil {
255		return nil, nil, err
256	}
257
258	i := new(Issue)
259	resp, err := s.client.Do(req, i)
260	if err != nil {
261		return nil, resp, err
262	}
263
264	return i, resp, err
265}
266
267// CreateIssueOptions represents the available CreateIssue() options.
268//
269// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#new-issues
270type CreateIssueOptions struct {
271	Title                              *string    `url:"title,omitempty" json:"title,omitempty"`
272	Description                        *string    `url:"description,omitempty" json:"description,omitempty"`
273	Confidential                       *bool      `url:"confidential,omitempty" json:"confidential,omitempty"`
274	AssigneeIDs                        []int      `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"`
275	MilestoneID                        *int       `url:"milestone_id,omitempty" json:"milestone_id,omitempty"`
276	Labels                             Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
277	CreatedAt                          *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
278	DueDate                            *ISOTime   `url:"due_date,omitempty" json:"due_date,omitempty"`
279	MergeRequestToResolveDiscussionsOf *int       `url:"merge_request_to_resolve_discussions_of,omitempty" json:"merge_request_to_resolve_discussions_of,omitempty"`
280	DiscussionToResolve                *string    `url:"discussion_to_resolve,omitempty" json:"discussion_to_resolve,omitempty"`
281	Weight                             *int       `url:"weight,omitempty" json:"weight,omitempty"`
282}
283
284// CreateIssue creates a new project issue.
285//
286// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#new-issues
287func (s *IssuesService) CreateIssue(pid interface{}, opt *CreateIssueOptions, options ...OptionFunc) (*Issue, *Response, error) {
288	project, err := parseID(pid)
289	if err != nil {
290		return nil, nil, err
291	}
292	u := fmt.Sprintf("projects/%s/issues", pathEscape(project))
293
294	req, err := s.client.NewRequest("POST", u, opt, options)
295	if err != nil {
296		return nil, nil, err
297	}
298
299	i := new(Issue)
300	resp, err := s.client.Do(req, i)
301	if err != nil {
302		return nil, resp, err
303	}
304
305	return i, resp, err
306}
307
308// UpdateIssueOptions represents the available UpdateIssue() options.
309//
310// GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#edit-issue
311type UpdateIssueOptions struct {
312	Title            *string    `url:"title,omitempty" json:"title,omitempty"`
313	Description      *string    `url:"description,omitempty" json:"description,omitempty"`
314	Confidential     *bool      `url:"confidential,omitempty" json:"confidential,omitempty"`
315	AssigneeIDs      []int      `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"`
316	MilestoneID      *int       `url:"milestone_id,omitempty" json:"milestone_id,omitempty"`
317	Labels           Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
318	StateEvent       *string    `url:"state_event,omitempty" json:"state_event,omitempty"`
319	UpdatedAt        *time.Time `url:"updated_at,omitempty" json:"updated_at,omitempty"`
320	DueDate          *ISOTime   `url:"due_date,omitempty" json:"due_date,omitempty"`
321	Weight           *int       `url:"weight,omitempty" json:"weight,omitempty"`
322	DiscussionLocked *bool      `url:"discussion_locked,omitempty" json:"discussion_locked,omitempty"`
323}
324
325// UpdateIssue updates an existing project issue. This function is also used
326// to mark an issue as closed.
327//
328// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#edit-issues
329func (s *IssuesService) UpdateIssue(pid interface{}, issue int, opt *UpdateIssueOptions, options ...OptionFunc) (*Issue, *Response, error) {
330	project, err := parseID(pid)
331	if err != nil {
332		return nil, nil, err
333	}
334	u := fmt.Sprintf("projects/%s/issues/%d", pathEscape(project), issue)
335
336	req, err := s.client.NewRequest("PUT", u, opt, options)
337	if err != nil {
338		return nil, nil, err
339	}
340
341	i := new(Issue)
342	resp, err := s.client.Do(req, i)
343	if err != nil {
344		return nil, resp, err
345	}
346
347	return i, resp, err
348}
349
350// DeleteIssue deletes a single project issue.
351//
352// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#delete-an-issue
353func (s *IssuesService) DeleteIssue(pid interface{}, issue int, options ...OptionFunc) (*Response, error) {
354	project, err := parseID(pid)
355	if err != nil {
356		return nil, err
357	}
358	u := fmt.Sprintf("projects/%s/issues/%d", pathEscape(project), issue)
359
360	req, err := s.client.NewRequest("DELETE", u, nil, options)
361	if err != nil {
362		return nil, err
363	}
364
365	return s.client.Do(req, nil)
366}
367
368// SubscribeToIssue subscribes the authenticated user to the given issue to
369// receive notifications. If the user is already subscribed to the issue, the
370// status code 304 is returned.
371//
372// GitLab API docs:
373// https://docs.gitlab.com/ce/api/merge_requests.html#subscribe-to-a-merge-request
374func (s *IssuesService) SubscribeToIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) {
375	project, err := parseID(pid)
376	if err != nil {
377		return nil, nil, err
378	}
379	u := fmt.Sprintf("projects/%s/issues/%d/subscribe", pathEscape(project), issue)
380
381	req, err := s.client.NewRequest("POST", u, nil, options)
382	if err != nil {
383		return nil, nil, err
384	}
385
386	i := new(Issue)
387	resp, err := s.client.Do(req, i)
388	if err != nil {
389		return nil, resp, err
390	}
391
392	return i, resp, err
393}
394
395// UnsubscribeFromIssue unsubscribes the authenticated user from the given
396// issue to not receive notifications from that merge request. If the user
397// is not subscribed to the issue, status code 304 is returned.
398//
399// GitLab API docs:
400// https://docs.gitlab.com/ce/api/merge_requests.html#unsubscribe-from-a-merge-request
401func (s *IssuesService) UnsubscribeFromIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) {
402	project, err := parseID(pid)
403	if err != nil {
404		return nil, nil, err
405	}
406	u := fmt.Sprintf("projects/%s/issues/%d/unsubscribe", pathEscape(project), issue)
407
408	req, err := s.client.NewRequest("POST", u, nil, options)
409	if err != nil {
410		return nil, nil, err
411	}
412
413	i := new(Issue)
414	resp, err := s.client.Do(req, i)
415	if err != nil {
416		return nil, resp, err
417	}
418
419	return i, resp, err
420}
421
422// ListMergeRequestsClosingIssueOptions represents the available
423// ListMergeRequestsClosingIssue() options.
424//
425// GitLab API docs:
426// https://docs.gitlab.com/ce/api/issues.html#list-merge-requests-that-will-close-issue-on-merge
427type ListMergeRequestsClosingIssueOptions ListOptions
428
429// ListMergeRequestsClosingIssue gets all the merge requests that will close
430// issue when merged.
431//
432// GitLab API docs:
433// https://docs.gitlab.com/ce/api/issues.html#list-merge-requests-that-will-close-issue-on-merge
434func (s *IssuesService) ListMergeRequestsClosingIssue(pid interface{}, issue int, opt *ListMergeRequestsClosingIssueOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) {
435	project, err := parseID(pid)
436	if err != nil {
437		return nil, nil, err
438	}
439	u := fmt.Sprintf("/projects/%s/issues/%d/closed_by", pathEscape(project), issue)
440
441	req, err := s.client.NewRequest("GET", u, opt, options)
442	if err != nil {
443		return nil, nil, err
444	}
445
446	var m []*MergeRequest
447	resp, err := s.client.Do(req, &m)
448	if err != nil {
449		return nil, resp, err
450	}
451
452	return m, resp, err
453}
454
455// ListMergeRequestsRelatedToIssueOptions represents the available
456// ListMergeRequestsRelatedToIssue() options.
457//
458// GitLab API docs:
459// https://docs.gitlab.com/ce/api/issues.html#list-merge-requests-related-to-issue
460type ListMergeRequestsRelatedToIssueOptions ListOptions
461
462// ListMergeRequestsRelatedToIssue gets all the merge requests that are
463// related to the issue
464//
465// GitLab API docs:
466// https://docs.gitlab.com/ce/api/issues.html#list-merge-requests-related-to-issue
467func (s *IssuesService) ListMergeRequestsRelatedToIssue(pid interface{}, issue int, opt *ListMergeRequestsRelatedToIssueOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) {
468	project, err := parseID(pid)
469	if err != nil {
470		return nil, nil, err
471	}
472	u := fmt.Sprintf("/projects/%s/issues/%d/related_merge_requests",
473		pathEscape(project),
474		issue,
475	)
476
477	req, err := s.client.NewRequest("GET", u, opt, options)
478	if err != nil {
479		return nil, nil, err
480	}
481
482	var m []*MergeRequest
483	resp, err := s.client.Do(req, &m)
484	if err != nil {
485		return nil, resp, err
486	}
487
488	return m, resp, err
489}
490
491// SetTimeEstimate sets the time estimate for a single project issue.
492//
493// GitLab API docs:
494// https://docs.gitlab.com/ce/api/issues.html#set-a-time-estimate-for-an-issue
495func (s *IssuesService) SetTimeEstimate(pid interface{}, issue int, opt *SetTimeEstimateOptions, options ...OptionFunc) (*TimeStats, *Response, error) {
496	return s.timeStats.setTimeEstimate(pid, "issues", issue, opt, options...)
497}
498
499// ResetTimeEstimate resets the time estimate for a single project issue.
500//
501// GitLab API docs:
502// https://docs.gitlab.com/ce/api/issues.html#reset-the-time-estimate-for-an-issue
503func (s *IssuesService) ResetTimeEstimate(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) {
504	return s.timeStats.resetTimeEstimate(pid, "issues", issue, options...)
505}
506
507// AddSpentTime adds spent time for a single project issue.
508//
509// GitLab API docs:
510// https://docs.gitlab.com/ce/api/issues.html#add-spent-time-for-an-issue
511func (s *IssuesService) AddSpentTime(pid interface{}, issue int, opt *AddSpentTimeOptions, options ...OptionFunc) (*TimeStats, *Response, error) {
512	return s.timeStats.addSpentTime(pid, "issues", issue, opt, options...)
513}
514
515// ResetSpentTime resets the spent time for a single project issue.
516//
517// GitLab API docs:
518// https://docs.gitlab.com/ce/api/issues.html#reset-spent-time-for-an-issue
519func (s *IssuesService) ResetSpentTime(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) {
520	return s.timeStats.resetSpentTime(pid, "issues", issue, options...)
521}
522
523// GetTimeSpent gets the spent time for a single project issue.
524//
525// GitLab API docs:
526// https://docs.gitlab.com/ce/api/issues.html#get-time-tracking-stats
527func (s *IssuesService) GetTimeSpent(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) {
528	return s.timeStats.getTimeSpent(pid, "issues", issue, options...)
529}
530