...

Source file src/github.com/xanzy/go-gitlab/issues.go

Documentation: github.com/xanzy/go-gitlab

     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  
    17  package gitlab
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"net/http"
    23  	"reflect"
    24  	"time"
    25  )
    26  
    27  // IssuesService handles communication with the issue related methods
    28  // of the GitLab API.
    29  //
    30  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html
    31  type IssuesService struct {
    32  	client    *Client
    33  	timeStats *timeStatsService
    34  }
    35  
    36  // IssueAuthor represents a author of the issue.
    37  type IssueAuthor struct {
    38  	ID        int    `json:"id"`
    39  	State     string `json:"state"`
    40  	WebURL    string `json:"web_url"`
    41  	Name      string `json:"name"`
    42  	AvatarURL string `json:"avatar_url"`
    43  	Username  string `json:"username"`
    44  }
    45  
    46  // IssueAssignee represents a assignee of the issue.
    47  type IssueAssignee struct {
    48  	ID        int    `json:"id"`
    49  	State     string `json:"state"`
    50  	WebURL    string `json:"web_url"`
    51  	Name      string `json:"name"`
    52  	AvatarURL string `json:"avatar_url"`
    53  	Username  string `json:"username"`
    54  }
    55  
    56  // IssueReferences represents references of the issue.
    57  type IssueReferences struct {
    58  	Short    string `json:"short"`
    59  	Relative string `json:"relative"`
    60  	Full     string `json:"full"`
    61  }
    62  
    63  // IssueCloser represents a closer of the issue.
    64  type IssueCloser struct {
    65  	ID        int    `json:"id"`
    66  	State     string `json:"state"`
    67  	WebURL    string `json:"web_url"`
    68  	Name      string `json:"name"`
    69  	AvatarURL string `json:"avatar_url"`
    70  	Username  string `json:"username"`
    71  }
    72  
    73  // IssueLinks represents links of the issue.
    74  type IssueLinks struct {
    75  	Self       string `json:"self"`
    76  	Notes      string `json:"notes"`
    77  	AwardEmoji string `json:"award_emoji"`
    78  	Project    string `json:"project"`
    79  }
    80  
    81  // Issue represents a GitLab issue.
    82  //
    83  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html
    84  type Issue struct {
    85  	ID                   int                    `json:"id"`
    86  	IID                  int                    `json:"iid"`
    87  	ExternalID           string                 `json:"external_id"`
    88  	State                string                 `json:"state"`
    89  	Description          string                 `json:"description"`
    90  	HealthStatus         string                 `json:"health_status"`
    91  	Author               *IssueAuthor           `json:"author"`
    92  	Milestone            *Milestone             `json:"milestone"`
    93  	ProjectID            int                    `json:"project_id"`
    94  	Assignees            []*IssueAssignee       `json:"assignees"`
    95  	Assignee             *IssueAssignee         `json:"assignee"`
    96  	UpdatedAt            *time.Time             `json:"updated_at"`
    97  	ClosedAt             *time.Time             `json:"closed_at"`
    98  	ClosedBy             *IssueCloser           `json:"closed_by"`
    99  	Title                string                 `json:"title"`
   100  	CreatedAt            *time.Time             `json:"created_at"`
   101  	MovedToID            int                    `json:"moved_to_id"`
   102  	Labels               Labels                 `json:"labels"`
   103  	LabelDetails         []*LabelDetails        `json:"label_details"`
   104  	Upvotes              int                    `json:"upvotes"`
   105  	Downvotes            int                    `json:"downvotes"`
   106  	DueDate              *ISOTime               `json:"due_date"`
   107  	WebURL               string                 `json:"web_url"`
   108  	References           *IssueReferences       `json:"references"`
   109  	TimeStats            *TimeStats             `json:"time_stats"`
   110  	Confidential         bool                   `json:"confidential"`
   111  	Weight               int                    `json:"weight"`
   112  	DiscussionLocked     bool                   `json:"discussion_locked"`
   113  	IssueType            *string                `json:"issue_type,omitempty"`
   114  	Subscribed           bool                   `json:"subscribed"`
   115  	UserNotesCount       int                    `json:"user_notes_count"`
   116  	Links                *IssueLinks            `json:"_links"`
   117  	IssueLinkID          int                    `json:"issue_link_id"`
   118  	MergeRequestCount    int                    `json:"merge_requests_count"`
   119  	EpicIssueID          int                    `json:"epic_issue_id"`
   120  	Epic                 *Epic                  `json:"epic"`
   121  	Iteration            *GroupIteration        `json:"iteration"`
   122  	TaskCompletionStatus *TasksCompletionStatus `json:"task_completion_status"`
   123  }
   124  
   125  func (i Issue) String() string {
   126  	return Stringify(i)
   127  }
   128  
   129  // UnmarshalJSON implements the json.Unmarshaler interface.
   130  func (i *Issue) UnmarshalJSON(data []byte) error {
   131  	type alias Issue
   132  
   133  	raw := make(map[string]interface{})
   134  	err := json.Unmarshal(data, &raw)
   135  	if err != nil {
   136  		return err
   137  	}
   138  
   139  	if reflect.TypeOf(raw["id"]).Kind() == reflect.String {
   140  		raw["external_id"] = raw["id"]
   141  		delete(raw, "id")
   142  	}
   143  
   144  	labelDetails, ok := raw["labels"].([]interface{})
   145  	if ok && len(labelDetails) > 0 {
   146  		// We only want to change anything if we got label details.
   147  		if _, ok := labelDetails[0].(map[string]interface{}); ok {
   148  			labels := make([]interface{}, len(labelDetails))
   149  			for i, details := range labelDetails {
   150  				labels[i] = details.(map[string]interface{})["name"]
   151  			}
   152  
   153  			// Set the correct values
   154  			raw["labels"] = labels
   155  			raw["label_details"] = labelDetails
   156  		}
   157  	}
   158  
   159  	data, err = json.Marshal(raw)
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	return json.Unmarshal(data, (*alias)(i))
   165  }
   166  
   167  // LabelDetails represents detailed label information.
   168  type LabelDetails struct {
   169  	ID              int    `json:"id"`
   170  	Name            string `json:"name"`
   171  	Color           string `json:"color"`
   172  	Description     string `json:"description"`
   173  	DescriptionHTML string `json:"description_html"`
   174  	TextColor       string `json:"text_color"`
   175  }
   176  
   177  // ListIssuesOptions represents the available ListIssues() options.
   178  //
   179  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#list-issues
   180  type ListIssuesOptions struct {
   181  	ListOptions
   182  	State               *string          `url:"state,omitempty" json:"state,omitempty"`
   183  	Labels              *LabelOptions    `url:"labels,comma,omitempty" json:"labels,omitempty"`
   184  	NotLabels           *LabelOptions    `url:"not[labels],comma,omitempty" json:"not[labels],omitempty"`
   185  	WithLabelDetails    *bool            `url:"with_labels_details,omitempty" json:"with_labels_details,omitempty"`
   186  	Milestone           *string          `url:"milestone,omitempty" json:"milestone,omitempty"`
   187  	NotMilestone        *string          `url:"not[milestone],omitempty" json:"not[milestone],omitempty"`
   188  	Scope               *string          `url:"scope,omitempty" json:"scope,omitempty"`
   189  	AuthorID            *int             `url:"author_id,omitempty" json:"author_id,omitempty"`
   190  	AuthorUsername      *string          `url:"author_username,omitempty" json:"author_username,omitempty"`
   191  	NotAuthorUsername   *string          `url:"not[author_username],omitempty" json:"not[author_username],omitempty"`
   192  	NotAuthorID         *[]int           `url:"not[author_id],omitempty" json:"not[author_id],omitempty"`
   193  	AssigneeID          *AssigneeIDValue `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
   194  	NotAssigneeID       *[]int           `url:"not[assignee_id],omitempty" json:"not[assignee_id],omitempty"`
   195  	AssigneeUsername    *string          `url:"assignee_username,omitempty" json:"assignee_username,omitempty"`
   196  	NotAssigneeUsername *string          `url:"not[assignee_username],omitempty" json:"not[assignee_username],omitempty"`
   197  	MyReactionEmoji     *string          `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
   198  	NotMyReactionEmoji  *[]string        `url:"not[my_reaction_emoji],omitempty" json:"not[my_reaction_emoji],omitempty"`
   199  	IIDs                *[]int           `url:"iids[],omitempty" json:"iids,omitempty"`
   200  	In                  *string          `url:"in,omitempty" json:"in,omitempty"`
   201  	NotIn               *string          `url:"not[in],omitempty" json:"not[in],omitempty"`
   202  	OrderBy             *string          `url:"order_by,omitempty" json:"order_by,omitempty"`
   203  	Sort                *string          `url:"sort,omitempty" json:"sort,omitempty"`
   204  	Search              *string          `url:"search,omitempty" json:"search,omitempty"`
   205  	NotSearch           *string          `url:"not[search],omitempty" json:"not[search],omitempty"`
   206  	CreatedAfter        *time.Time       `url:"created_after,omitempty" json:"created_after,omitempty"`
   207  	CreatedBefore       *time.Time       `url:"created_before,omitempty" json:"created_before,omitempty"`
   208  	DueDate             *string          `url:"due_date,omitempty" json:"due_date,omitempty"`
   209  	UpdatedAfter        *time.Time       `url:"updated_after,omitempty" json:"updated_after,omitempty"`
   210  	UpdatedBefore       *time.Time       `url:"updated_before,omitempty" json:"updated_before,omitempty"`
   211  	Confidential        *bool            `url:"confidential,omitempty" json:"confidential,omitempty"`
   212  	IssueType           *string          `url:"issue_type,omitempty" json:"issue_type,omitempty"`
   213  	IterationID         *int             `url:"iteration_id,omitempty" json:"iteration_id,omitempty"`
   214  }
   215  
   216  // ListIssues gets all issues created by authenticated user. This function
   217  // takes pagination parameters page and per_page to restrict the list of issues.
   218  //
   219  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#list-issues
   220  func (s *IssuesService) ListIssues(opt *ListIssuesOptions, options ...RequestOptionFunc) ([]*Issue, *Response, error) {
   221  	req, err := s.client.NewRequest(http.MethodGet, "issues", opt, options)
   222  	if err != nil {
   223  		return nil, nil, err
   224  	}
   225  
   226  	var i []*Issue
   227  	resp, err := s.client.Do(req, &i)
   228  	if err != nil {
   229  		return nil, resp, err
   230  	}
   231  
   232  	return i, resp, nil
   233  }
   234  
   235  // ListGroupIssuesOptions represents the available ListGroupIssues() options.
   236  //
   237  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#list-group-issues
   238  type ListGroupIssuesOptions struct {
   239  	ListOptions
   240  	State             *string       `url:"state,omitempty" json:"state,omitempty"`
   241  	Labels            *LabelOptions `url:"labels,comma,omitempty" json:"labels,omitempty"`
   242  	NotLabels         *LabelOptions `url:"not[labels],comma,omitempty" json:"not[labels],omitempty"`
   243  	WithLabelDetails  *bool         `url:"with_labels_details,omitempty" json:"with_labels_details,omitempty"`
   244  	IIDs              *[]int        `url:"iids[],omitempty" json:"iids,omitempty"`
   245  	Milestone         *string       `url:"milestone,omitempty" json:"milestone,omitempty"`
   246  	NotMilestone      *string       `url:"not[milestone],omitempty" json:"not[milestone],omitempty"`
   247  	Scope             *string       `url:"scope,omitempty" json:"scope,omitempty"`
   248  	AuthorID          *int          `url:"author_id,omitempty" json:"author_id,omitempty"`
   249  	NotAuthorID       *[]int        `url:"not[author_id],omitempty" json:"not[author_id],omitempty"`
   250  	AuthorUsername    *string       `url:"author_username,omitempty" json:"author_username,omitempty"`
   251  	NotAuthorUsername *string       `url:"not[author_username],omitempty" json:"not[author_username],omitempty"`
   252  
   253  	AssigneeID          *AssigneeIDValue `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
   254  	NotAssigneeID       *[]int           `url:"not[assignee_id],omitempty" json:"not[assignee_id],omitempty"`
   255  	AssigneeUsername    *string          `url:"assignee_username,omitempty" json:"assignee_username,omitempty"`
   256  	NotAssigneeUsername *string          `url:"not[assignee_username],omitempty" json:"not[assignee_username],omitempty"`
   257  	MyReactionEmoji     *string          `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
   258  	NotMyReactionEmoji  *[]string        `url:"not[my_reaction_emoji],omitempty" json:"not[my_reaction_emoji],omitempty"`
   259  	OrderBy             *string          `url:"order_by,omitempty" json:"order_by,omitempty"`
   260  	Sort                *string          `url:"sort,omitempty" json:"sort,omitempty"`
   261  	Search              *string          `url:"search,omitempty" json:"search,omitempty"`
   262  	NotSearch           *string          `url:"not[search],omitempty" json:"not[search],omitempty"`
   263  	In                  *string          `url:"in,omitempty" json:"in,omitempty"`
   264  	NotIn               *string          `url:"not[in],omitempty" json:"not[in],omitempty"`
   265  	CreatedAfter        *time.Time       `url:"created_after,omitempty" json:"created_after,omitempty"`
   266  	CreatedBefore       *time.Time       `url:"created_before,omitempty" json:"created_before,omitempty"`
   267  	DueDate             *string          `url:"due_date,omitempty" json:"due_date,omitempty"`
   268  	UpdatedAfter        *time.Time       `url:"updated_after,omitempty" json:"updated_after,omitempty"`
   269  	UpdatedBefore       *time.Time       `url:"updated_before,omitempty" json:"updated_before,omitempty"`
   270  	IssueType           *string          `url:"issue_type,omitempty" json:"issue_type,omitempty"`
   271  	IterationID         *int             `url:"iteration_id,omitempty" json:"iteration_id,omitempty"`
   272  }
   273  
   274  // ListGroupIssues gets a list of group issues. This function accepts
   275  // pagination parameters page and per_page to return the list of group issues.
   276  //
   277  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#list-group-issues
   278  func (s *IssuesService) ListGroupIssues(pid interface{}, opt *ListGroupIssuesOptions, options ...RequestOptionFunc) ([]*Issue, *Response, error) {
   279  	group, err := parseID(pid)
   280  	if err != nil {
   281  		return nil, nil, err
   282  	}
   283  	u := fmt.Sprintf("groups/%s/issues", PathEscape(group))
   284  
   285  	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
   286  	if err != nil {
   287  		return nil, nil, err
   288  	}
   289  
   290  	var i []*Issue
   291  	resp, err := s.client.Do(req, &i)
   292  	if err != nil {
   293  		return nil, resp, err
   294  	}
   295  
   296  	return i, resp, nil
   297  }
   298  
   299  // ListProjectIssuesOptions represents the available ListProjectIssues() options.
   300  //
   301  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#list-project-issues
   302  type ListProjectIssuesOptions struct {
   303  	ListOptions
   304  	IIDs                *[]int           `url:"iids[],omitempty" json:"iids,omitempty"`
   305  	State               *string          `url:"state,omitempty" json:"state,omitempty"`
   306  	Labels              *LabelOptions    `url:"labels,comma,omitempty" json:"labels,omitempty"`
   307  	NotLabels           *LabelOptions    `url:"not[labels],comma,omitempty" json:"not[labels],omitempty"`
   308  	WithLabelDetails    *bool            `url:"with_labels_details,omitempty" json:"with_labels_details,omitempty"`
   309  	Milestone           *string          `url:"milestone,omitempty" json:"milestone,omitempty"`
   310  	NotMilestone        *string          `url:"not[milestone],omitempty" json:"not[milestone],omitempty"`
   311  	Scope               *string          `url:"scope,omitempty" json:"scope,omitempty"`
   312  	AuthorID            *int             `url:"author_id,omitempty" json:"author_id,omitempty"`
   313  	AuthorUsername      *string          `url:"author_username,omitempty" json:"author_username,omitempty"`
   314  	NotAuthorUsername   *string          `url:"not[author_username],omitempty" json:"not[author_username],omitempty"`
   315  	NotAuthorID         *[]int           `url:"not[author_id],omitempty" json:"not[author_id],omitempty"`
   316  	AssigneeID          *AssigneeIDValue `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
   317  	NotAssigneeID       *[]int           `url:"not[assignee_id],omitempty" json:"not[assignee_id],omitempty"`
   318  	AssigneeUsername    *string          `url:"assignee_username,omitempty" json:"assignee_username,omitempty"`
   319  	NotAssigneeUsername *string          `url:"not[assignee_username],omitempty" json:"not[assignee_username],omitempty"`
   320  	MyReactionEmoji     *string          `url:"my_reaction_emoji,omitempty" json:"my_reaction_emoji,omitempty"`
   321  	NotMyReactionEmoji  *[]string        `url:"not[my_reaction_emoji],omitempty" json:"not[my_reaction_emoji],omitempty"`
   322  	OrderBy             *string          `url:"order_by,omitempty" json:"order_by,omitempty"`
   323  	Sort                *string          `url:"sort,omitempty" json:"sort,omitempty"`
   324  	Search              *string          `url:"search,omitempty" json:"search,omitempty"`
   325  	In                  *string          `url:"in,omitempty" json:"in,omitempty"`
   326  	NotIn               *string          `url:"not[in],omitempty" json:"not[in],omitempty"`
   327  	CreatedAfter        *time.Time       `url:"created_after,omitempty" json:"created_after,omitempty"`
   328  	CreatedBefore       *time.Time       `url:"created_before,omitempty" json:"created_before,omitempty"`
   329  	DueDate             *string          `url:"due_date,omitempty" json:"due_date,omitempty"`
   330  	UpdatedAfter        *time.Time       `url:"updated_after,omitempty" json:"updated_after,omitempty"`
   331  	UpdatedBefore       *time.Time       `url:"updated_before,omitempty" json:"updated_before,omitempty"`
   332  	Confidential        *bool            `url:"confidential,omitempty" json:"confidential,omitempty"`
   333  	IssueType           *string          `url:"issue_type,omitempty" json:"issue_type,omitempty"`
   334  	IterationID         *int             `url:"iteration_id,omitempty" json:"iteration_id,omitempty"`
   335  }
   336  
   337  // ListProjectIssues gets a list of project issues. This function accepts
   338  // pagination parameters page and per_page to return the list of project issues.
   339  //
   340  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#list-project-issues
   341  func (s *IssuesService) ListProjectIssues(pid interface{}, opt *ListProjectIssuesOptions, options ...RequestOptionFunc) ([]*Issue, *Response, error) {
   342  	project, err := parseID(pid)
   343  	if err != nil {
   344  		return nil, nil, err
   345  	}
   346  	u := fmt.Sprintf("projects/%s/issues", PathEscape(project))
   347  
   348  	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
   349  	if err != nil {
   350  		return nil, nil, err
   351  	}
   352  
   353  	var i []*Issue
   354  	resp, err := s.client.Do(req, &i)
   355  	if err != nil {
   356  		return nil, resp, err
   357  	}
   358  
   359  	return i, resp, nil
   360  }
   361  
   362  // GetIssueByID gets a single issue.
   363  //
   364  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#single-issue
   365  func (s *IssuesService) GetIssueByID(issue int, options ...RequestOptionFunc) (*Issue, *Response, error) {
   366  	u := fmt.Sprintf("issues/%d", issue)
   367  
   368  	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
   369  	if err != nil {
   370  		return nil, nil, err
   371  	}
   372  
   373  	i := new(Issue)
   374  	resp, err := s.client.Do(req, i)
   375  	if err != nil {
   376  		return nil, resp, err
   377  	}
   378  
   379  	return i, resp, nil
   380  }
   381  
   382  // GetIssue gets a single project issue.
   383  //
   384  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#single-project-issue
   385  func (s *IssuesService) GetIssue(pid interface{}, issue int, options ...RequestOptionFunc) (*Issue, *Response, error) {
   386  	project, err := parseID(pid)
   387  	if err != nil {
   388  		return nil, nil, err
   389  	}
   390  	u := fmt.Sprintf("projects/%s/issues/%d", PathEscape(project), issue)
   391  
   392  	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
   393  	if err != nil {
   394  		return nil, nil, err
   395  	}
   396  
   397  	i := new(Issue)
   398  	resp, err := s.client.Do(req, i)
   399  	if err != nil {
   400  		return nil, resp, err
   401  	}
   402  
   403  	return i, resp, nil
   404  }
   405  
   406  // CreateIssueOptions represents the available CreateIssue() options.
   407  //
   408  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#new-issue
   409  type CreateIssueOptions struct {
   410  	IID                                *int          `url:"iid,omitempty" json:"iid,omitempty"`
   411  	Title                              *string       `url:"title,omitempty" json:"title,omitempty"`
   412  	Description                        *string       `url:"description,omitempty" json:"description,omitempty"`
   413  	Confidential                       *bool         `url:"confidential,omitempty" json:"confidential,omitempty"`
   414  	AssigneeIDs                        *[]int        `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"`
   415  	MilestoneID                        *int          `url:"milestone_id,omitempty" json:"milestone_id,omitempty"`
   416  	Labels                             *LabelOptions `url:"labels,comma,omitempty" json:"labels,omitempty"`
   417  	CreatedAt                          *time.Time    `url:"created_at,omitempty" json:"created_at,omitempty"`
   418  	DueDate                            *ISOTime      `url:"due_date,omitempty" json:"due_date,omitempty"`
   419  	EpicID                             *int          `url:"epic_id,omitempty" json:"epic_id,omitempty"`
   420  	MergeRequestToResolveDiscussionsOf *int          `url:"merge_request_to_resolve_discussions_of,omitempty" json:"merge_request_to_resolve_discussions_of,omitempty"`
   421  	DiscussionToResolve                *string       `url:"discussion_to_resolve,omitempty" json:"discussion_to_resolve,omitempty"`
   422  	Weight                             *int          `url:"weight,omitempty" json:"weight,omitempty"`
   423  	IssueType                          *string       `url:"issue_type,omitempty" json:"issue_type,omitempty"`
   424  }
   425  
   426  // CreateIssue creates a new project issue.
   427  //
   428  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#new-issue
   429  func (s *IssuesService) CreateIssue(pid interface{}, opt *CreateIssueOptions, options ...RequestOptionFunc) (*Issue, *Response, error) {
   430  	project, err := parseID(pid)
   431  	if err != nil {
   432  		return nil, nil, err
   433  	}
   434  	u := fmt.Sprintf("projects/%s/issues", PathEscape(project))
   435  
   436  	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
   437  	if err != nil {
   438  		return nil, nil, err
   439  	}
   440  
   441  	i := new(Issue)
   442  	resp, err := s.client.Do(req, i)
   443  	if err != nil {
   444  		return nil, resp, err
   445  	}
   446  
   447  	return i, resp, nil
   448  }
   449  
   450  // UpdateIssueOptions represents the available UpdateIssue() options.
   451  //
   452  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#edit-issue
   453  type UpdateIssueOptions struct {
   454  	Title            *string       `url:"title,omitempty" json:"title,omitempty"`
   455  	Description      *string       `url:"description,omitempty" json:"description,omitempty"`
   456  	Confidential     *bool         `url:"confidential,omitempty" json:"confidential,omitempty"`
   457  	AssigneeIDs      *[]int        `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"`
   458  	MilestoneID      *int          `url:"milestone_id,omitempty" json:"milestone_id,omitempty"`
   459  	Labels           *LabelOptions `url:"labels,comma,omitempty" json:"labels,omitempty"`
   460  	AddLabels        *LabelOptions `url:"add_labels,comma,omitempty" json:"add_labels,omitempty"`
   461  	RemoveLabels     *LabelOptions `url:"remove_labels,comma,omitempty" json:"remove_labels,omitempty"`
   462  	StateEvent       *string       `url:"state_event,omitempty" json:"state_event,omitempty"`
   463  	UpdatedAt        *time.Time    `url:"updated_at,omitempty" json:"updated_at,omitempty"`
   464  	DueDate          *ISOTime      `url:"due_date,omitempty" json:"due_date,omitempty"`
   465  	EpicID           *int          `url:"epic_id,omitempty" json:"epic_id,omitempty"`
   466  	Weight           *int          `url:"weight,omitempty" json:"weight,omitempty"`
   467  	DiscussionLocked *bool         `url:"discussion_locked,omitempty" json:"discussion_locked,omitempty"`
   468  	IssueType        *string       `url:"issue_type,omitempty" json:"issue_type,omitempty"`
   469  }
   470  
   471  // UpdateIssue updates an existing project issue. This function is also used
   472  // to mark an issue as closed.
   473  //
   474  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#edit-issues
   475  func (s *IssuesService) UpdateIssue(pid interface{}, issue int, opt *UpdateIssueOptions, options ...RequestOptionFunc) (*Issue, *Response, error) {
   476  	project, err := parseID(pid)
   477  	if err != nil {
   478  		return nil, nil, err
   479  	}
   480  	u := fmt.Sprintf("projects/%s/issues/%d", PathEscape(project), issue)
   481  
   482  	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
   483  	if err != nil {
   484  		return nil, nil, err
   485  	}
   486  
   487  	i := new(Issue)
   488  	resp, err := s.client.Do(req, i)
   489  	if err != nil {
   490  		return nil, resp, err
   491  	}
   492  
   493  	return i, resp, nil
   494  }
   495  
   496  // DeleteIssue deletes a single project issue.
   497  //
   498  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#delete-an-issue
   499  func (s *IssuesService) DeleteIssue(pid interface{}, issue int, options ...RequestOptionFunc) (*Response, error) {
   500  	project, err := parseID(pid)
   501  	if err != nil {
   502  		return nil, err
   503  	}
   504  	u := fmt.Sprintf("projects/%s/issues/%d", PathEscape(project), issue)
   505  
   506  	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
   507  	if err != nil {
   508  		return nil, err
   509  	}
   510  
   511  	return s.client.Do(req, nil)
   512  }
   513  
   514  // ReorderIssueOptions represents the available ReorderIssue() options.
   515  //
   516  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#reorder-an-issue
   517  type ReorderIssueOptions struct {
   518  	MoveAfterID  *int `url:"move_after_id,omitempty" json:"move_after_id,omitempty"`
   519  	MoveBeforeID *int `url:"move_before_id,omitempty" json:"move_before_id,omitempty"`
   520  }
   521  
   522  // ReorderIssue reorders an issue.
   523  //
   524  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#reorder-an-issue
   525  func (s *IssuesService) ReorderIssue(pid interface{}, issue int, opt *ReorderIssueOptions, options ...RequestOptionFunc) (*Issue, *Response, error) {
   526  	project, err := parseID(pid)
   527  	if err != nil {
   528  		return nil, nil, err
   529  	}
   530  	u := fmt.Sprintf("projects/%s/issues/%d/reorder", PathEscape(project), issue)
   531  
   532  	req, err := s.client.NewRequest(http.MethodPut, u, opt, options)
   533  	if err != nil {
   534  		return nil, nil, err
   535  	}
   536  
   537  	i := new(Issue)
   538  	resp, err := s.client.Do(req, i)
   539  	if err != nil {
   540  		return nil, resp, err
   541  	}
   542  
   543  	return i, resp, nil
   544  }
   545  
   546  // MoveIssueOptions represents the available MoveIssue() options.
   547  //
   548  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#move-an-issue
   549  type MoveIssueOptions struct {
   550  	ToProjectID *int `url:"to_project_id,omitempty" json:"to_project_id,omitempty"`
   551  }
   552  
   553  // MoveIssue updates an existing project issue. This function is also used
   554  // to mark an issue as closed.
   555  //
   556  // GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#move-an-issue
   557  func (s *IssuesService) MoveIssue(pid interface{}, issue int, opt *MoveIssueOptions, options ...RequestOptionFunc) (*Issue, *Response, error) {
   558  	project, err := parseID(pid)
   559  	if err != nil {
   560  		return nil, nil, err
   561  	}
   562  	u := fmt.Sprintf("projects/%s/issues/%d/move", PathEscape(project), issue)
   563  
   564  	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
   565  	if err != nil {
   566  		return nil, nil, err
   567  	}
   568  
   569  	i := new(Issue)
   570  	resp, err := s.client.Do(req, i)
   571  	if err != nil {
   572  		return nil, resp, err
   573  	}
   574  
   575  	return i, resp, nil
   576  }
   577  
   578  // SubscribeToIssue subscribes the authenticated user to the given issue to
   579  // receive notifications. If the user is already subscribed to the issue, the
   580  // status code 304 is returned.
   581  //
   582  // GitLab API docs:
   583  // https://docs.gitlab.com/ee/api/issues.html#subscribe-to-an-issue
   584  func (s *IssuesService) SubscribeToIssue(pid interface{}, issue int, options ...RequestOptionFunc) (*Issue, *Response, error) {
   585  	project, err := parseID(pid)
   586  	if err != nil {
   587  		return nil, nil, err
   588  	}
   589  	u := fmt.Sprintf("projects/%s/issues/%d/subscribe", PathEscape(project), issue)
   590  
   591  	req, err := s.client.NewRequest(http.MethodPost, u, nil, options)
   592  	if err != nil {
   593  		return nil, nil, err
   594  	}
   595  
   596  	i := new(Issue)
   597  	resp, err := s.client.Do(req, i)
   598  	if err != nil {
   599  		return nil, resp, err
   600  	}
   601  
   602  	return i, resp, nil
   603  }
   604  
   605  // UnsubscribeFromIssue unsubscribes the authenticated user from the given
   606  // issue to not receive notifications from that merge request. If the user
   607  // is not subscribed to the issue, status code 304 is returned.
   608  //
   609  // GitLab API docs:
   610  // https://docs.gitlab.com/ee/api/issues.html#unsubscribe-from-an-issue
   611  func (s *IssuesService) UnsubscribeFromIssue(pid interface{}, issue int, options ...RequestOptionFunc) (*Issue, *Response, error) {
   612  	project, err := parseID(pid)
   613  	if err != nil {
   614  		return nil, nil, err
   615  	}
   616  	u := fmt.Sprintf("projects/%s/issues/%d/unsubscribe", PathEscape(project), issue)
   617  
   618  	req, err := s.client.NewRequest(http.MethodPost, u, nil, options)
   619  	if err != nil {
   620  		return nil, nil, err
   621  	}
   622  
   623  	i := new(Issue)
   624  	resp, err := s.client.Do(req, i)
   625  	if err != nil {
   626  		return nil, resp, err
   627  	}
   628  
   629  	return i, resp, nil
   630  }
   631  
   632  // CreateTodo creates a todo for the current user for an issue.
   633  // If there already exists a todo for the user on that issue, status code
   634  // 304 is returned.
   635  //
   636  // GitLab API docs:
   637  // https://docs.gitlab.com/ee/api/issues.html#create-a-to-do-item
   638  func (s *IssuesService) CreateTodo(pid interface{}, issue int, options ...RequestOptionFunc) (*Todo, *Response, error) {
   639  	project, err := parseID(pid)
   640  	if err != nil {
   641  		return nil, nil, err
   642  	}
   643  	u := fmt.Sprintf("projects/%s/issues/%d/todo", PathEscape(project), issue)
   644  
   645  	req, err := s.client.NewRequest(http.MethodPost, u, nil, options)
   646  	if err != nil {
   647  		return nil, nil, err
   648  	}
   649  
   650  	t := new(Todo)
   651  	resp, err := s.client.Do(req, t)
   652  	if err != nil {
   653  		return nil, resp, err
   654  	}
   655  
   656  	return t, resp, nil
   657  }
   658  
   659  // ListMergeRequestsClosingIssueOptions represents the available
   660  // ListMergeRequestsClosingIssue() options.
   661  //
   662  // GitLab API docs:
   663  // https://docs.gitlab.com/ee/api/issues.html#list-merge-requests-that-close-a-particular-issue-on-merge
   664  type ListMergeRequestsClosingIssueOptions ListOptions
   665  
   666  // ListMergeRequestsClosingIssue gets all the merge requests that will close
   667  // issue when merged.
   668  //
   669  // GitLab API docs:
   670  // https://docs.gitlab.com/ee/api/issues.html#list-merge-requests-that-close-a-particular-issue-on-merge
   671  func (s *IssuesService) ListMergeRequestsClosingIssue(pid interface{}, issue int, opt *ListMergeRequestsClosingIssueOptions, options ...RequestOptionFunc) ([]*MergeRequest, *Response, error) {
   672  	project, err := parseID(pid)
   673  	if err != nil {
   674  		return nil, nil, err
   675  	}
   676  	u := fmt.Sprintf("projects/%s/issues/%d/closed_by", PathEscape(project), issue)
   677  
   678  	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
   679  	if err != nil {
   680  		return nil, nil, err
   681  	}
   682  
   683  	var m []*MergeRequest
   684  	resp, err := s.client.Do(req, &m)
   685  	if err != nil {
   686  		return nil, resp, err
   687  	}
   688  
   689  	return m, resp, nil
   690  }
   691  
   692  // ListMergeRequestsRelatedToIssueOptions represents the available
   693  // ListMergeRequestsRelatedToIssue() options.
   694  //
   695  // GitLab API docs:
   696  // https://docs.gitlab.com/ee/api/issues.html#list-merge-requests-related-to-issue
   697  type ListMergeRequestsRelatedToIssueOptions ListOptions
   698  
   699  // ListMergeRequestsRelatedToIssue gets all the merge requests that are
   700  // related to the issue
   701  //
   702  // GitLab API docs:
   703  // https://docs.gitlab.com/ee/api/issues.html#list-merge-requests-related-to-issue
   704  func (s *IssuesService) ListMergeRequestsRelatedToIssue(pid interface{}, issue int, opt *ListMergeRequestsRelatedToIssueOptions, options ...RequestOptionFunc) ([]*MergeRequest, *Response, error) {
   705  	project, err := parseID(pid)
   706  	if err != nil {
   707  		return nil, nil, err
   708  	}
   709  	u := fmt.Sprintf("projects/%s/issues/%d/related_merge_requests",
   710  		PathEscape(project),
   711  		issue,
   712  	)
   713  
   714  	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
   715  	if err != nil {
   716  		return nil, nil, err
   717  	}
   718  
   719  	var m []*MergeRequest
   720  	resp, err := s.client.Do(req, &m)
   721  	if err != nil {
   722  		return nil, resp, err
   723  	}
   724  
   725  	return m, resp, nil
   726  }
   727  
   728  // SetTimeEstimate sets the time estimate for a single project issue.
   729  //
   730  // GitLab API docs:
   731  // https://docs.gitlab.com/ee/api/issues.html#set-a-time-estimate-for-an-issue
   732  func (s *IssuesService) SetTimeEstimate(pid interface{}, issue int, opt *SetTimeEstimateOptions, options ...RequestOptionFunc) (*TimeStats, *Response, error) {
   733  	return s.timeStats.setTimeEstimate(pid, "issues", issue, opt, options...)
   734  }
   735  
   736  // ResetTimeEstimate resets the time estimate for a single project issue.
   737  //
   738  // GitLab API docs:
   739  // https://docs.gitlab.com/ee/api/issues.html#reset-the-time-estimate-for-an-issue
   740  func (s *IssuesService) ResetTimeEstimate(pid interface{}, issue int, options ...RequestOptionFunc) (*TimeStats, *Response, error) {
   741  	return s.timeStats.resetTimeEstimate(pid, "issues", issue, options...)
   742  }
   743  
   744  // AddSpentTime adds spent time for a single project issue.
   745  //
   746  // GitLab API docs:
   747  // https://docs.gitlab.com/ee/api/issues.html#add-spent-time-for-an-issue
   748  func (s *IssuesService) AddSpentTime(pid interface{}, issue int, opt *AddSpentTimeOptions, options ...RequestOptionFunc) (*TimeStats, *Response, error) {
   749  	return s.timeStats.addSpentTime(pid, "issues", issue, opt, options...)
   750  }
   751  
   752  // ResetSpentTime resets the spent time for a single project issue.
   753  //
   754  // GitLab API docs:
   755  // https://docs.gitlab.com/ee/api/issues.html#reset-spent-time-for-an-issue
   756  func (s *IssuesService) ResetSpentTime(pid interface{}, issue int, options ...RequestOptionFunc) (*TimeStats, *Response, error) {
   757  	return s.timeStats.resetSpentTime(pid, "issues", issue, options...)
   758  }
   759  
   760  // GetTimeSpent gets the spent time for a single project issue.
   761  //
   762  // GitLab API docs:
   763  // https://docs.gitlab.com/ee/api/issues.html#get-time-tracking-stats
   764  func (s *IssuesService) GetTimeSpent(pid interface{}, issue int, options ...RequestOptionFunc) (*TimeStats, *Response, error) {
   765  	return s.timeStats.getTimeSpent(pid, "issues", issue, options...)
   766  }
   767  
   768  // GetParticipants gets a list of issue participants.
   769  //
   770  // GitLab API docs:
   771  // https://docs.gitlab.com/ee/api/issues.html#participants-on-issues
   772  func (s *IssuesService) GetParticipants(pid interface{}, issue int, options ...RequestOptionFunc) ([]*BasicUser, *Response, error) {
   773  	project, err := parseID(pid)
   774  	if err != nil {
   775  		return nil, nil, err
   776  	}
   777  	u := fmt.Sprintf("projects/%s/issues/%d/participants", PathEscape(project), issue)
   778  
   779  	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
   780  	if err != nil {
   781  		return nil, nil, err
   782  	}
   783  
   784  	var bu []*BasicUser
   785  	resp, err := s.client.Do(req, &bu)
   786  	if err != nil {
   787  		return nil, resp, err
   788  	}
   789  
   790  	return bu, resp, nil
   791  }
   792  

View as plain text