1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
5 // Somewhat inspired by https://github.com/mattn/go-redmine (MIT licensed)
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.
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"`
49 type IssueFilter struct {
58 type issuesResult struct {
59 Issues []Issue `json:"issues"`
60 TotalCount uint `json:"total_count"`
61 Offset uint `json:"offset"`
62 Limit uint `json:"limit"`
65 type issueWrapper struct {
66 Issue Issue `json:"issue"`
69 // issueFilters converts an *IssueFilter into a slice of filter strings
70 func issueFilters(issueFilter *IssueFilter) []string {
71 var filterParameters []string
73 if issueFilter == nil {
74 return filterParameters
77 if len(issueFilter.ProjectID) > 0 {
78 filterParameters = append(filterParameters, fmt.Sprintf("project_id=%v", issueFilter.ProjectID))
80 if len(issueFilter.StatusID) > 0 {
81 filterParameters = append(filterParameters, fmt.Sprintf("status_id=%v", issueFilter.StatusID))
83 if len(issueFilter.ParentID) > 0 {
84 filterParameters = append(filterParameters, fmt.Sprintf("parent_id=%v", issueFilter.ParentID))
86 if len(issueFilter.Subject) > 0 {
87 filterParameters = append(filterParameters, fmt.Sprintf("subject=~%v", issueFilter.Subject))
89 if len(issueFilter.VersionID) > 0 {
90 filterParameters = append(filterParameters, fmt.Sprintf("fixed_version_id=%v", issueFilter.VersionID))
92 if len(issueFilter.ReleaseID) > 0 {
93 filterParameters = append(filterParameters, fmt.Sprintf("release_id=%v", issueFilter.ReleaseID))
96 return filterParameters
99 // FilteredIssues returns a slice of issues that matches the f criteria
100 // This function handles pagination internally, so it could return a lot
101 // of results at once.
102 func (c *Client) FilteredIssues(f *IssueFilter) ([]Issue, error) {
107 // Get 100 results at once (the default is 25)
110 parameters := append(s, fmt.Sprintf("offset=%d", offset), fmt.Sprintf("limit=%d", limit))
111 res, err := c.Get("/issues.json?" + strings.Join(parameters, "&"))
115 defer res.Body.Close()
118 err = responseHelper(res, &r, 200)
122 issues = append(issues, r.Issues...)
123 if r.Offset+uint(len(r.Issues)) >= r.TotalCount {
132 // CreateIssue creates a redmine issue
133 func (c *Client) CreateIssue(issue Issue) (*Issue, error) {
136 s, err := json.Marshal(ir)
140 res, err := c.Post("/issues.json", string(s))
144 defer res.Body.Close()
147 err = responseHelper(res, &r, 201)
154 // GetIssue retrieves a redmine Issue object by id
155 func (c *Client) GetIssue(ID int) (*Issue, error) {
156 res, err := c.Get("/issues/" + strconv.Itoa(ID) + ".json")
160 defer res.Body.Close()
162 if res.StatusCode == 404 {
163 return nil, fmt.Errorf("Issue with id %d not found", ID)
166 err = responseHelper(res, &r, 200)
173 // UpdateIssue updates a redmine issue
174 func (c *Client) UpdateIssue(issue Issue) error {
176 issue.ProjectID = issue.Project.ID
178 s, err := json.Marshal(ir)
182 res, err := c.Put("/issues/"+strconv.Itoa(issue.ID)+".json", string(s))
186 defer res.Body.Close()
187 if res.StatusCode == 404 {
188 return fmt.Errorf("Issue with id %d not found", issue.ID)
191 return responseHelper(res, nil, 204)
194 // FindOrCreateIssue finds or creates an issue with a given subject, parentID, versionID and projectID
195 func (c *Client) FindOrCreateIssue(subject string, parentID int, versionID int, projectID int) (Issue, error) {
198 f.Subject = url.QueryEscape(subject)
200 f.ParentID = strconv.Itoa(parentID)
203 f.ProjectID = strconv.Itoa(projectID)
206 issues, err := c.FilteredIssues(&f)
211 // Issue found, return it
212 return issues[0], err
216 issue.ProjectID = projectID
217 issue.FixedVersionID = versionID
218 issue.Subject = subject
220 issue.ParentIssueID = parentID
223 i, err := c.CreateIssue(issue)
230 // SetRelease updates the release for an issue
231 func (c *Client) SetRelease(issue Issue, release int) error {
232 issue.ReleaseID = release
234 return c.UpdateIssue(issue)
237 // SetSprint updates the sprint (fixed_version) for an issue
238 func (c *Client) SetSprint(issue Issue, version int) error {
239 issue.FixedVersionID = version
240 issue.FixedVersion = nil
241 return c.UpdateIssue(issue)
244 // SetStatus updates the status for an issue
245 func (c *Client) SetStatus(issue Issue, status int) error {
246 issue.StatusID = status
248 return c.UpdateIssue(issue)