Salt installer change: standardize on putting the certs directory under
[arvados-dev.git] / lib / redmine / issues.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 // Somewhat inspired by https://github.com/mattn/go-redmine (MIT licensed)
6
7 package redmine
8
9 import (
10         "encoding/json"
11         "fmt"
12         "net/url"
13         "strconv"
14         "strings"
15 )
16
17 // In read operations the Redmine API returns ID fields like ProjectID.
18 // When updating or creating an object, it wants a Project field.
19 // This struct represents both for convenience.
20 type Issue struct {
21         ID             int                `json:"id"`
22         Subject        string             `json:"subject"`
23         Description    string             `json:"description,omitempty"`
24         ProjectID      int                `json:"project_id,omitempty"`
25         Project        *IDName            `json:"project,omitempty"`
26         ParentIssueID  int                `json:"parent_issue_id,omitempty"`
27         Parent         *ID                `json:"parent,omitempty"`
28         StatusID       int                `json:"status_id,omitempty"`
29         Status         *IDName            `json:"status,omitempty"`
30         FixedVersionID int                `json:"fixed_version_id,omitempty"`
31         FixedVersion   *IDName            `json:"fixed_version,omitempty"`
32         ReleaseID      int                `json:"release_id,omitempty"`
33         Release        map[string]*IDName `json:"release,omitempty"`
34         TrackerID      int                `json:"tracker_id,omitempty"`
35         Tracker        *IDName            `json:"tracker,omitempty"`
36         PriorityID     int                `json:"priority_id,omitempty"`
37         Priority       *IDName            `json:"priority,omitempty"`
38         CategoryID     int                `json:"category_id,omitempty"`
39         Category       *IDName            `json:"category,omitempty"`
40         AssignedToID   int                `json:"assigned_to_id,omitempty"`
41         AssignedTo     *IDName            `json:"assigned_to,omitempty"`
42         WatcherUserIDs []int              `json:"watcher_user_ids,omitempty"`
43         Watchers       []*IDName          `json:"watchers,omitempty"`
44         IsPrivate      bool               `json:"is_private,omitempty"`
45         EstimatedHours float64            `json:"estimated_hours,omitempty"`
46         Notes          string             `json:"notes,omitempty"`
47 }
48
49 type IssueFilter struct {
50         ProjectID string
51         StatusID  string
52         Subject   string
53         ParentID  string
54         VersionID string
55 }
56
57 type issuesResult struct {
58         Issues     []Issue `json:"issues"`
59         TotalCount uint    `json:"total_count"`
60         Offset     uint    `json:"offset"`
61         Limit      uint    `json:"limit"`
62 }
63
64 type issueWrapper struct {
65         Issue Issue `json:"issue"`
66 }
67
68 // issueFilters converts an *IssueFilter into a slice of filter strings
69 func issueFilters(issueFilter *IssueFilter) []string {
70         var filterParameters []string
71
72         if issueFilter == nil {
73                 return filterParameters
74         }
75
76         if len(issueFilter.ProjectID) > 0 {
77                 filterParameters = append(filterParameters, fmt.Sprintf("project_id=%v", issueFilter.ProjectID))
78         }
79         if len(issueFilter.StatusID) > 0 {
80                 filterParameters = append(filterParameters, fmt.Sprintf("status_id=%v", issueFilter.StatusID))
81         }
82         if len(issueFilter.ParentID) > 0 {
83                 filterParameters = append(filterParameters, fmt.Sprintf("parent_id=%v", issueFilter.ParentID))
84         }
85         if len(issueFilter.Subject) > 0 {
86                 filterParameters = append(filterParameters, fmt.Sprintf("subject=~%v", issueFilter.Subject))
87         }
88         if len(issueFilter.VersionID) > 0 {
89                 filterParameters = append(filterParameters, fmt.Sprintf("fixed_version_id=%v", issueFilter.VersionID))
90         }
91
92         return filterParameters
93 }
94
95 // FilteredIssues returns a slice of issues that matches the f criteria
96 // This function handles pagination internally, so it could return a lot
97 // of results at once.
98 func (c *Client) FilteredIssues(f *IssueFilter) ([]Issue, error) {
99         s := issueFilters(f)
100
101         var issues []Issue
102         var offset int
103         // Get 100 results at once (the default is 25)
104         limit := 100
105         for {
106                 parameters := append(s, fmt.Sprintf("offset=%d", offset), fmt.Sprintf("limit=%d", limit))
107                 res, err := c.Get("/issues.json?" + strings.Join(parameters, "&"))
108                 if err != nil {
109                         return nil, err
110                 }
111                 defer res.Body.Close()
112
113                 var r issuesResult
114                 err = responseHelper(res, &r, 200)
115                 if err != nil {
116                         return nil, err
117                 }
118                 issues = append(issues, r.Issues...)
119                 if r.Offset+uint(len(r.Issues)) >= r.TotalCount {
120                         break
121                 }
122                 offset += limit
123         }
124
125         return issues, nil
126 }
127
128 // CreateIssue creates a redmine issue
129 func (c *Client) CreateIssue(issue Issue) (*Issue, error) {
130         var ir issueWrapper
131         ir.Issue = issue
132         s, err := json.Marshal(ir)
133         if err != nil {
134                 return nil, err
135         }
136         res, err := c.Post("/issues.json", string(s))
137         if err != nil {
138                 return nil, err
139         }
140         defer res.Body.Close()
141
142         var r issueWrapper
143         err = responseHelper(res, &r, 201)
144         if err != nil {
145                 return nil, err
146         }
147         return &r.Issue, nil
148 }
149
150 // GetIssue retrieves a redmine Issue object by id
151 func (c *Client) GetIssue(ID int) (*Issue, error) {
152         res, err := c.Get("/issues/" + strconv.Itoa(ID) + ".json")
153         if err != nil {
154                 return nil, err
155         }
156         defer res.Body.Close()
157
158         if res.StatusCode == 404 {
159                 return nil, fmt.Errorf("Issue with id %d not found", ID)
160         }
161         var r issueWrapper
162         err = responseHelper(res, &r, 200)
163         if err != nil {
164                 return nil, err
165         }
166         return &r.Issue, nil
167 }
168
169 // UpdateIssue updates a redmine issue
170 func (c *Client) UpdateIssue(issue Issue) error {
171         var ir issueWrapper
172         issue.ProjectID = issue.Project.ID
173         ir.Issue = issue
174         s, err := json.Marshal(ir)
175         if err != nil {
176                 return err
177         }
178         res, err := c.Put("/issues/"+strconv.Itoa(issue.ID)+".json", string(s))
179         if err != nil {
180                 return err
181         }
182         defer res.Body.Close()
183         if res.StatusCode == 404 {
184                 return fmt.Errorf("Issue with id %d not found", issue.ID)
185         }
186
187         return responseHelper(res, nil, 200)
188 }
189
190 // FindOrCreateIssue finds or creates an issue with a given subject, parentID, versionID and projectID
191 func (c *Client) FindOrCreateIssue(subject string, parentID int, versionID int, projectID int) (Issue, error) {
192         var f IssueFilter
193         var issue Issue
194         f.Subject = url.QueryEscape(subject)
195         if parentID != 0 {
196                 f.ParentID = strconv.Itoa(parentID)
197         }
198         if projectID != 0 {
199                 f.ProjectID = strconv.Itoa(projectID)
200         }
201         f.StatusID = "*"
202         issues, err := c.FilteredIssues(&f)
203         if err != nil {
204                 return issue, err
205         }
206         if len(issues) > 0 {
207                 // Issue found, return it
208                 return issues[0], err
209         }
210
211         // Create new issue
212         issue.ProjectID = projectID
213         issue.FixedVersionID = versionID
214         issue.Subject = subject
215         if parentID != 0 {
216                 issue.ParentIssueID = parentID
217         }
218
219         i, err := c.CreateIssue(issue)
220         if err != nil {
221                 return Issue{}, err
222         }
223         return *i, err
224 }
225
226 // SetRelease updates the release for an issue
227 func (c *Client) SetRelease(issue Issue, release int) error {
228         issue.ReleaseID = release
229         issue.Release = nil
230         return c.UpdateIssue(issue)
231 }