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