Files
jira/issue.go
T
Mark Harrison 7bfa241547 Add URL escaping to an additional issuetype call
In #34 a fix was made to escape the issue type to support issue types
with spaces in. This is an additional place where the issue type needs
escaping, otherwise you get 400 Bad Request when trying to create an
issue for an issuetype that has a space in.
2017-10-28 12:21:07 -07:00

588 lines
17 KiB
Go

package jira
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/url"
"strings"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
)
type IssueQueryProvider interface {
ProvideIssueQueryString() string
}
type IssueOptions struct {
Fields []string `json:"fields,omitempty" yaml:"fields,omitempty"`
Expand []string `json:"expand,omitempty" yaml:"expand,omitempty"`
Properties []string `json:"properties,omitempty" yaml:"properties,omitempty"`
FieldsByKeys bool `json:"fieldsByKeys,omitempty" yaml:"fieldsByKeys,omitempty"`
UpdateHistory bool `json:"updateHistory,omitempty" yaml:"updateHistory,omitempty"`
}
func (o *IssueOptions) ProvideIssueQueryString() string {
params := []string{}
if len(o.Fields) > 0 {
params = append(params, "fields="+strings.Join(o.Fields, ","))
}
if len(o.Expand) > 0 {
params = append(params, "expand="+strings.Join(o.Expand, ","))
}
if len(o.Properties) > 0 {
params = append(params, "properties="+strings.Join(o.Properties, ","))
}
if o.FieldsByKeys {
params = append(params, "fieldsByKeys=true")
}
if o.UpdateHistory {
params = append(params, "updateHistory=true")
}
if len(params) > 0 {
return "?" + strings.Join(params, "&")
}
return ""
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getIssue
func (j *Jira) GetIssue(issue string, iqg IssueQueryProvider) (*jiradata.Issue, error) {
return GetIssue(j.UA, j.Endpoint, issue, iqg)
}
func GetIssue(ua HttpClient, endpoint string, issue string, iqg IssueQueryProvider) (*jiradata.Issue, error) {
query := ""
if iqg != nil {
query = iqg.ProvideIssueQueryString()
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s%s", endpoint, issue, query)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.Issue{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
func (j *Jira) GetIssueWorklog(issue string) (*jiradata.Worklogs, error) {
return GetIssueWorklog(j.UA, j.Endpoint, issue)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/worklog-getIssueWorklog
func GetIssueWorklog(ua HttpClient, endpoint string, issue string) (*jiradata.Worklogs, error) {
startAt := 0
total := 1
maxResults := 100
worklogs := jiradata.Worklogs{}
for startAt < total {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/worklog?startAt=%d&maxResults=%d", endpoint, issue, startAt, maxResults)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.WorklogWithPagination{}
err := readJSON(resp.Body, results)
if err != nil {
return nil, err
}
startAt = startAt + maxResults
total = results.Total
for _, worklog := range results.Worklogs {
worklogs = append(worklogs, worklog)
}
} else {
return nil, responseError(resp)
}
}
return &worklogs, nil
}
type WorklogProvider interface {
ProvideWorklog() *jiradata.Worklog
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/worklog-addWorklog
func (j *Jira) AddIssueWorklog(issue string, wp WorklogProvider) (*jiradata.Worklog, error) {
return AddIssueWorklog(j.UA, j.Endpoint, issue, wp)
}
func AddIssueWorklog(ua HttpClient, endpoint string, issue string, wp WorklogProvider) (*jiradata.Worklog, error) {
req := wp.ProvideWorklog()
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/worklog", endpoint, issue)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 201 {
results := &jiradata.Worklog{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getEditIssueMeta
func (j *Jira) GetIssueEditMeta(issue string) (*jiradata.EditMeta, error) {
return GetIssueEditMeta(j.UA, j.Endpoint, issue)
}
func GetIssueEditMeta(ua HttpClient, endpoint string, issue string) (*jiradata.EditMeta, error) {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/editmeta", endpoint, issue)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.EditMeta{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
type IssueUpdateProvider interface {
ProvideIssueUpdate() *jiradata.IssueUpdate
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue
func (j *Jira) EditIssue(issue string, iup IssueUpdateProvider) error {
return EditIssue(j.UA, j.Endpoint, issue, iup)
}
func EditIssue(ua HttpClient, endpoint string, issue string, iup IssueUpdateProvider) error {
req := iup.ProvideIssueUpdate()
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", endpoint, issue)
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-createIssue
func (j *Jira) CreateIssue(iup IssueUpdateProvider) (*jiradata.IssueCreateResponse, error) {
return CreateIssue(j.UA, j.Endpoint, iup)
}
func CreateIssue(ua HttpClient, endpoint string, iup IssueUpdateProvider) (*jiradata.IssueCreateResponse, error) {
req := iup.ProvideIssueUpdate()
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s/rest/api/2/issue", endpoint)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 201 {
results := &jiradata.IssueCreateResponse{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getCreateIssueMeta
func (j *Jira) GetIssueCreateMetaProject(projectKey string) (*jiradata.CreateMetaProject, error) {
return GetIssueCreateMetaProject(j.UA, j.Endpoint, projectKey)
}
func GetIssueCreateMetaProject(ua HttpClient, endpoint string, projectKey string) (*jiradata.CreateMetaProject, error) {
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&expand=projects.issuetypes.fields", endpoint, projectKey)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.CreateMeta{}
err = readJSON(resp.Body, results)
if err != nil {
return nil, err
}
for _, project := range results.Projects {
if project.Key == projectKey {
return project, nil
}
}
return nil, fmt.Errorf("Project %s not found", projectKey)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getCreateIssueMeta
func (j *Jira) GetIssueCreateMetaIssueType(projectKey, issueTypeName string) (*jiradata.IssueType, error) {
return GetIssueCreateMetaIssueType(j.UA, j.Endpoint, projectKey, issueTypeName)
}
func GetIssueCreateMetaIssueType(ua HttpClient, endpoint string, projectKey, issueTypeName string) (*jiradata.IssueType, error) {
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", endpoint, projectKey, url.QueryEscape(issueTypeName))
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.CreateMeta{}
err = readJSON(resp.Body, results)
if err != nil {
return nil, err
}
for _, project := range results.Projects {
if project.Key == projectKey {
for _, issueType := range project.IssueTypes {
if issueType.Name == issueTypeName {
return issueType, nil
}
}
}
}
return nil, fmt.Errorf("Project %s and IssueType %s not found", projectKey, issueTypeName)
}
return nil, responseError(resp)
}
type LinkIssueProvider interface {
ProvideLinkIssueRequest() *jiradata.LinkIssueRequest
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issueLink-linkIssues
func (j *Jira) LinkIssues(lip LinkIssueProvider) error {
return LinkIssues(j.UA, j.Endpoint, lip)
}
func LinkIssues(ua HttpClient, endpoint string, lip LinkIssueProvider) error {
req := lip.ProvideLinkIssueRequest()
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issueLink", endpoint)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 201 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getTransitions
func (j *Jira) GetIssueTransitions(issue string) (*jiradata.TransitionsMeta, error) {
return GetIssueTransitions(j.UA, j.Endpoint, issue)
}
func GetIssueTransitions(ua HttpClient, endpoint string, issue string) (*jiradata.TransitionsMeta, error) {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", endpoint, issue)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.TransitionsMeta{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-doTransition
func (j *Jira) TransitionIssue(issue string, iup IssueUpdateProvider) error {
return TransitionIssue(j.UA, j.Endpoint, issue, iup)
}
func TransitionIssue(ua HttpClient, endpoint string, issue string, iup IssueUpdateProvider) error {
req := iup.ProvideIssueUpdate()
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", endpoint, issue)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issueLinkType-getIssueLinkTypes
func (j *Jira) GetIssueLinkTypes() (*jiradata.IssueLinkTypes, error) {
return GetIssueLinkTypes(j.UA, j.Endpoint)
}
func GetIssueLinkTypes(ua HttpClient, endpoint string) (*jiradata.IssueLinkTypes, error) {
uri := fmt.Sprintf("%s/rest/api/2/issueLinkType", endpoint)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := struct {
IssueLinkTypes jiradata.IssueLinkTypes
}{
IssueLinkTypes: jiradata.IssueLinkTypes{},
}
return &results.IssueLinkTypes, readJSON(resp.Body, &results)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-addVote
func (j *Jira) IssueAddVote(issue string) error {
return IssueAddVote(j.UA, j.Endpoint, issue)
}
func IssueAddVote(ua HttpClient, endpoint string, issue string) error {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", endpoint, issue)
resp, err := ua.Post(uri, "application/json", strings.NewReader("{}"))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-removeVote
func (j *Jira) IssueRemoveVote(issue string) error {
return IssueRemoveVote(j.UA, j.Endpoint, issue)
}
func IssueRemoveVote(ua HttpClient, endpoint string, issue string) error {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", endpoint, issue)
resp, err := ua.Delete(uri)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
type RankRequestProvider interface {
ProvideRankRequest() *jiradata.RankRequest
}
// https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/issue-rankIssues
func (j *Jira) RankIssues(rrp RankRequestProvider) error {
return RankIssues(j.UA, j.Endpoint, rrp)
}
func RankIssues(ua HttpClient, endpoint string, rrp RankRequestProvider) error {
req := rrp.ProvideRankRequest()
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/agile/1.0/issue/rank", endpoint)
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-addWatcher
func (j *Jira) IssueAddWatcher(issue, user string) error {
return IssueAddWatcher(j.UA, j.Endpoint, issue, user)
}
func IssueAddWatcher(ua HttpClient, endpoint string, issue, user string) error {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", endpoint, issue)
resp, err := ua.Post(uri, "application/json", strings.NewReader(fmt.Sprintf("%q", user)))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-addWatcher
func (j *Jira) IssueRemoveWatcher(issue, user string) error {
return IssueRemoveWatcher(j.UA, j.Endpoint, issue, user)
}
func IssueRemoveWatcher(ua HttpClient, endpoint string, issue, user string) error {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers?username=%s", endpoint, issue, user)
resp, err := ua.Delete(uri)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
type CommentProvider interface {
ProvideComment() *jiradata.Comment
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/comment-addComment
func (j *Jira) IssueAddComment(issue string, cp CommentProvider) (*jiradata.Comment, error) {
return IssueAddComment(j.UA, j.Endpoint, issue, cp)
}
func IssueAddComment(ua HttpClient, endpoint string, issue string, cp CommentProvider) (*jiradata.Comment, error) {
req := cp.ProvideComment()
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/comment", endpoint, issue)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 201 {
results := jiradata.Comment{}
return &results, readJSON(resp.Body, &results)
}
return nil, responseError(resp)
}
type UserProvider interface {
ProvideUser() *jiradata.User
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-assign
func (j *Jira) IssueAssign(issue, name string) error {
return IssueAssign(j.UA, j.Endpoint, issue, name)
}
func IssueAssign(ua HttpClient, endpoint string, issue, name string) error {
// this is special, not using the jiradata.User structure
// because we need to be able to send `null` as the name param
// when we want to un-assign the issue
req := struct {
Name *string `json:"name"`
}{&name}
if name == "" {
req.Name = nil
}
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/assignee", endpoint, issue)
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/attachments-addAttachment
func (j *Jira) IssueAttachFile(issue, filename string, contents io.Reader) (*jiradata.ListOfAttachment, error) {
return IssueAttachFile(j.UA, j.Endpoint, issue, filename, contents)
}
func IssueAttachFile(ua HttpClient, endpoint string, issue, filename string, contents io.Reader) (*jiradata.ListOfAttachment, error) {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
formFile, err := w.CreateFormFile("file", filename)
if err != nil {
return nil, err
}
_, err = io.Copy(formFile, contents)
if err != nil {
return nil, err
}
uri, err := url.Parse(fmt.Sprintf("%s/rest/api/2/issue/%s/attachments", endpoint, issue))
req := oreo.RequestBuilder(uri).WithMethod("POST").WithHeader(
"X-Atlassian-Token", "no-check",
).WithHeader(
"Accept", "application/json",
).WithContentType(w.FormDataContentType()).WithBody(&buf).Build()
w.Close()
resp, err := ua.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
// FIXME move this to a test, and run go tests as part of our regression
if false {
// this is because schema is wrong, defaults to type `int`, so we manually change it
// to `string`. If the jiradata is regenerated we need to manually make the change
// again.
log.Debugf("Assert Attachment.ID is a string, rather than int: %v", &jiradata.Attachment{
ID: jiradata.IntOrString(0),
})
}
results := jiradata.ListOfAttachment{}
return &results, readJSON(resp.Body, &results)
}
return nil, responseError(resp)
}