issues.go

  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	In              *string    `url:"in,omitempty" json:"in,omitempty"`
166	CreatedAfter    *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
167	CreatedBefore   *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
168	UpdatedAfter    *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
169	UpdatedBefore   *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"`
170}
171
172// ListGroupIssues gets a list of group issues. This function accepts
173// pagination parameters page and per_page to return the list of group issues.
174//
175// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-group-issues
176func (s *IssuesService) ListGroupIssues(pid interface{}, opt *ListGroupIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) {
177	group, err := parseID(pid)
178	if err != nil {
179		return nil, nil, err
180	}
181	u := fmt.Sprintf("groups/%s/issues", pathEscape(group))
182
183	req, err := s.client.NewRequest("GET", u, opt, options)
184	if err != nil {
185		return nil, nil, err
186	}
187
188	var i []*Issue
189	resp, err := s.client.Do(req, &i)
190	if err != nil {
191		return nil, resp, err
192	}
193
194	return i, resp, err
195}
196
197// ListProjectIssuesOptions represents the available ListProjectIssues() options.
198//
199// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-project-issues
200type ListProjectIssuesOptions struct {
201	ListOptions
202	IIDs            []int      `url:"iids[],omitempty" json:"iids,omitempty"`
203	State           *string    `url:"state,omitempty" json:"state,omitempty"`
204	Labels          Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
205	Milestone       *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
206	Scope           *string    `url:"scope,omitempty" json:"scope,omitempty"`
207	AuthorID        *int       `url:"author_id,omitempty" json:"author_id,omitempty"`
208	AssigneeID      *int       `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
209	MyReactionEmoji *string    `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
210	OrderBy         *string    `url:"order_by,omitempty" json:"order_by,omitempty"`
211	Sort            *string    `url:"sort,omitempty" json:"sort,omitempty"`
212	Search          *string    `url:"search,omitempty" json:"search,omitempty"`
213	In              *string    `url:"in,omitempty" json:"in,omitempty"`
214	CreatedAfter    *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
215	CreatedBefore   *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
216	UpdatedAfter    *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
217	UpdatedBefore   *time.Time `url:"updated_before,omitempty" json:"updated_before,omitempty"`
218}
219
220// ListProjectIssues gets a list of project issues. This function accepts
221// pagination parameters page and per_page to return the list of project issues.
222//
223// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#list-project-issues
224func (s *IssuesService) ListProjectIssues(pid interface{}, opt *ListProjectIssuesOptions, options ...OptionFunc) ([]*Issue, *Response, error) {
225	project, err := parseID(pid)
226	if err != nil {
227		return nil, nil, err
228	}
229	u := fmt.Sprintf("projects/%s/issues", pathEscape(project))
230
231	req, err := s.client.NewRequest("GET", u, opt, options)
232	if err != nil {
233		return nil, nil, err
234	}
235
236	var i []*Issue
237	resp, err := s.client.Do(req, &i)
238	if err != nil {
239		return nil, resp, err
240	}
241
242	return i, resp, err
243}
244
245// GetIssue gets a single project issue.
246//
247// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#single-issues
248func (s *IssuesService) GetIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) {
249	project, err := parseID(pid)
250	if err != nil {
251		return nil, nil, err
252	}
253	u := fmt.Sprintf("projects/%s/issues/%d", pathEscape(project), issue)
254
255	req, err := s.client.NewRequest("GET", u, nil, options)
256	if err != nil {
257		return nil, nil, err
258	}
259
260	i := new(Issue)
261	resp, err := s.client.Do(req, i)
262	if err != nil {
263		return nil, resp, err
264	}
265
266	return i, resp, err
267}
268
269// CreateIssueOptions represents the available CreateIssue() options.
270//
271// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#new-issues
272type CreateIssueOptions struct {
273	IID                                *int       `url:"iid,omitempty" json:"iid,omitempty"`
274	Title                              *string    `url:"title,omitempty" json:"title,omitempty"`
275	Description                        *string    `url:"description,omitempty" json:"description,omitempty"`
276	Confidential                       *bool      `url:"confidential,omitempty" json:"confidential,omitempty"`
277	AssigneeIDs                        []int      `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"`
278	MilestoneID                        *int       `url:"milestone_id,omitempty" json:"milestone_id,omitempty"`
279	Labels                             Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
280	CreatedAt                          *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
281	DueDate                            *ISOTime   `url:"due_date,omitempty" json:"due_date,omitempty"`
282	MergeRequestToResolveDiscussionsOf *int       `url:"merge_request_to_resolve_discussions_of,omitempty" json:"merge_request_to_resolve_discussions_of,omitempty"`
283	DiscussionToResolve                *string    `url:"discussion_to_resolve,omitempty" json:"discussion_to_resolve,omitempty"`
284	Weight                             *int       `url:"weight,omitempty" json:"weight,omitempty"`
285}
286
287// CreateIssue creates a new project issue.
288//
289// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#new-issues
290func (s *IssuesService) CreateIssue(pid interface{}, opt *CreateIssueOptions, options ...OptionFunc) (*Issue, *Response, error) {
291	project, err := parseID(pid)
292	if err != nil {
293		return nil, nil, err
294	}
295	u := fmt.Sprintf("projects/%s/issues", pathEscape(project))
296
297	req, err := s.client.NewRequest("POST", u, opt, options)
298	if err != nil {
299		return nil, nil, err
300	}
301
302	i := new(Issue)
303	resp, err := s.client.Do(req, i)
304	if err != nil {
305		return nil, resp, err
306	}
307
308	return i, resp, err
309}
310
311// UpdateIssueOptions represents the available UpdateIssue() options.
312//
313// GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#edit-issue
314type UpdateIssueOptions struct {
315	Title            *string    `url:"title,omitempty" json:"title,omitempty"`
316	Description      *string    `url:"description,omitempty" json:"description,omitempty"`
317	Confidential     *bool      `url:"confidential,omitempty" json:"confidential,omitempty"`
318	AssigneeIDs      []int      `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"`
319	MilestoneID      *int       `url:"milestone_id,omitempty" json:"milestone_id,omitempty"`
320	Labels           Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
321	StateEvent       *string    `url:"state_event,omitempty" json:"state_event,omitempty"`
322	UpdatedAt        *time.Time `url:"updated_at,omitempty" json:"updated_at,omitempty"`
323	DueDate          *ISOTime   `url:"due_date,omitempty" json:"due_date,omitempty"`
324	Weight           *int       `url:"weight,omitempty" json:"weight,omitempty"`
325	DiscussionLocked *bool      `url:"discussion_locked,omitempty" json:"discussion_locked,omitempty"`
326}
327
328// UpdateIssue updates an existing project issue. This function is also used
329// to mark an issue as closed.
330//
331// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#edit-issues
332func (s *IssuesService) UpdateIssue(pid interface{}, issue int, opt *UpdateIssueOptions, options ...OptionFunc) (*Issue, *Response, error) {
333	project, err := parseID(pid)
334	if err != nil {
335		return nil, nil, err
336	}
337	u := fmt.Sprintf("projects/%s/issues/%d", pathEscape(project), issue)
338
339	req, err := s.client.NewRequest("PUT", u, opt, options)
340	if err != nil {
341		return nil, nil, err
342	}
343
344	i := new(Issue)
345	resp, err := s.client.Do(req, i)
346	if err != nil {
347		return nil, resp, err
348	}
349
350	return i, resp, err
351}
352
353// DeleteIssue deletes a single project issue.
354//
355// GitLab API docs: https://docs.gitlab.com/ce/api/issues.html#delete-an-issue
356func (s *IssuesService) DeleteIssue(pid interface{}, issue int, options ...OptionFunc) (*Response, error) {
357	project, err := parseID(pid)
358	if err != nil {
359		return nil, err
360	}
361	u := fmt.Sprintf("projects/%s/issues/%d", pathEscape(project), issue)
362
363	req, err := s.client.NewRequest("DELETE", u, nil, options)
364	if err != nil {
365		return nil, err
366	}
367
368	return s.client.Do(req, nil)
369}
370
371// SubscribeToIssue subscribes the authenticated user to the given issue to
372// receive notifications. If the user is already subscribed to the issue, the
373// status code 304 is returned.
374//
375// GitLab API docs:
376// https://docs.gitlab.com/ce/api/merge_requests.html#subscribe-to-a-merge-request
377func (s *IssuesService) SubscribeToIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) {
378	project, err := parseID(pid)
379	if err != nil {
380		return nil, nil, err
381	}
382	u := fmt.Sprintf("projects/%s/issues/%d/subscribe", pathEscape(project), issue)
383
384	req, err := s.client.NewRequest("POST", u, nil, options)
385	if err != nil {
386		return nil, nil, err
387	}
388
389	i := new(Issue)
390	resp, err := s.client.Do(req, i)
391	if err != nil {
392		return nil, resp, err
393	}
394
395	return i, resp, err
396}
397
398// UnsubscribeFromIssue unsubscribes the authenticated user from the given
399// issue to not receive notifications from that merge request. If the user
400// is not subscribed to the issue, status code 304 is returned.
401//
402// GitLab API docs:
403// https://docs.gitlab.com/ce/api/merge_requests.html#unsubscribe-from-a-merge-request
404func (s *IssuesService) UnsubscribeFromIssue(pid interface{}, issue int, options ...OptionFunc) (*Issue, *Response, error) {
405	project, err := parseID(pid)
406	if err != nil {
407		return nil, nil, err
408	}
409	u := fmt.Sprintf("projects/%s/issues/%d/unsubscribe", pathEscape(project), issue)
410
411	req, err := s.client.NewRequest("POST", u, nil, options)
412	if err != nil {
413		return nil, nil, err
414	}
415
416	i := new(Issue)
417	resp, err := s.client.Do(req, i)
418	if err != nil {
419		return nil, resp, err
420	}
421
422	return i, resp, err
423}
424
425// ListMergeRequestsClosingIssueOptions represents the available
426// ListMergeRequestsClosingIssue() options.
427//
428// GitLab API docs:
429// https://docs.gitlab.com/ce/api/issues.html#list-merge-requests-that-will-close-issue-on-merge
430type ListMergeRequestsClosingIssueOptions ListOptions
431
432// ListMergeRequestsClosingIssue gets all the merge requests that will close
433// issue when merged.
434//
435// GitLab API docs:
436// https://docs.gitlab.com/ce/api/issues.html#list-merge-requests-that-will-close-issue-on-merge
437func (s *IssuesService) ListMergeRequestsClosingIssue(pid interface{}, issue int, opt *ListMergeRequestsClosingIssueOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) {
438	project, err := parseID(pid)
439	if err != nil {
440		return nil, nil, err
441	}
442	u := fmt.Sprintf("/projects/%s/issues/%d/closed_by", pathEscape(project), issue)
443
444	req, err := s.client.NewRequest("GET", u, opt, options)
445	if err != nil {
446		return nil, nil, err
447	}
448
449	var m []*MergeRequest
450	resp, err := s.client.Do(req, &m)
451	if err != nil {
452		return nil, resp, err
453	}
454
455	return m, resp, err
456}
457
458// ListMergeRequestsRelatedToIssueOptions represents the available
459// ListMergeRequestsRelatedToIssue() options.
460//
461// GitLab API docs:
462// https://docs.gitlab.com/ce/api/issues.html#list-merge-requests-related-to-issue
463type ListMergeRequestsRelatedToIssueOptions ListOptions
464
465// ListMergeRequestsRelatedToIssue gets all the merge requests that are
466// related to the issue
467//
468// GitLab API docs:
469// https://docs.gitlab.com/ce/api/issues.html#list-merge-requests-related-to-issue
470func (s *IssuesService) ListMergeRequestsRelatedToIssue(pid interface{}, issue int, opt *ListMergeRequestsRelatedToIssueOptions, options ...OptionFunc) ([]*MergeRequest, *Response, error) {
471	project, err := parseID(pid)
472	if err != nil {
473		return nil, nil, err
474	}
475	u := fmt.Sprintf("/projects/%s/issues/%d/related_merge_requests",
476		pathEscape(project),
477		issue,
478	)
479
480	req, err := s.client.NewRequest("GET", u, opt, options)
481	if err != nil {
482		return nil, nil, err
483	}
484
485	var m []*MergeRequest
486	resp, err := s.client.Do(req, &m)
487	if err != nil {
488		return nil, resp, err
489	}
490
491	return m, resp, err
492}
493
494// SetTimeEstimate sets the time estimate for a single project issue.
495//
496// GitLab API docs:
497// https://docs.gitlab.com/ce/api/issues.html#set-a-time-estimate-for-an-issue
498func (s *IssuesService) SetTimeEstimate(pid interface{}, issue int, opt *SetTimeEstimateOptions, options ...OptionFunc) (*TimeStats, *Response, error) {
499	return s.timeStats.setTimeEstimate(pid, "issues", issue, opt, options...)
500}
501
502// ResetTimeEstimate resets the time estimate for a single project issue.
503//
504// GitLab API docs:
505// https://docs.gitlab.com/ce/api/issues.html#reset-the-time-estimate-for-an-issue
506func (s *IssuesService) ResetTimeEstimate(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) {
507	return s.timeStats.resetTimeEstimate(pid, "issues", issue, options...)
508}
509
510// AddSpentTime adds spent time for a single project issue.
511//
512// GitLab API docs:
513// https://docs.gitlab.com/ce/api/issues.html#add-spent-time-for-an-issue
514func (s *IssuesService) AddSpentTime(pid interface{}, issue int, opt *AddSpentTimeOptions, options ...OptionFunc) (*TimeStats, *Response, error) {
515	return s.timeStats.addSpentTime(pid, "issues", issue, opt, options...)
516}
517
518// ResetSpentTime resets the spent time for a single project issue.
519//
520// GitLab API docs:
521// https://docs.gitlab.com/ce/api/issues.html#reset-spent-time-for-an-issue
522func (s *IssuesService) ResetSpentTime(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) {
523	return s.timeStats.resetSpentTime(pid, "issues", issue, options...)
524}
525
526// GetTimeSpent gets the spent time for a single project issue.
527//
528// GitLab API docs:
529// https://docs.gitlab.com/ce/api/issues.html#get-time-tracking-stats
530func (s *IssuesService) GetTimeSpent(pid interface{}, issue int, options ...OptionFunc) (*TimeStats, *Response, error) {
531	return s.timeStats.getTimeSpent(pid, "issues", issue, options...)
532}