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}