2015-09-03 12:25:21 +02:00
package jira
import (
2016-05-19 23:11:21 +02:00
"bytes"
2015-09-03 12:25:21 +02:00
"fmt"
2016-05-19 23:11:21 +02:00
"io"
"mime/multipart"
2015-09-03 12:25:21 +02:00
"net/http"
2016-05-29 17:30:45 +02:00
"net/url"
"strings"
"time"
2015-09-03 12:25:21 +02:00
)
const (
// AssigneeAutomatic represents the value of the "Assignee: Automatic" of JIRA
AssigneeAutomatic = "-1"
)
// IssueService handles Issues for the JIRA instance / API.
//
2016-03-27 14:24:48 +02:00
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue
2015-09-03 12:25:21 +02:00
type IssueService struct {
client * Client
}
// Issue represents a JIRA issue.
type Issue struct {
Expand string ` json:"expand,omitempty" `
ID string ` json:"id,omitempty" `
Self string ` json:"self,omitempty" `
Key string ` json:"key,omitempty" `
Fields * IssueFields ` json:"fields,omitempty" `
}
2016-05-19 23:11:21 +02:00
// Attachment represents a JIRA attachment
type Attachment struct {
2016-06-03 23:14:27 +02:00
Self string ` json:"self,omitempty" `
ID string ` json:"id,omitempty" `
Filename string ` json:"filename,omitempty" `
Author * User ` json:"author,omitempty" `
Created string ` json:"created,omitempty" `
Size int ` json:"size,omitempty" `
MimeType string ` json:"mimeType,omitempty" `
Content string ` json:"content,omitempty" `
Thumbnail string ` json:"thumbnail,omitempty" `
2016-05-19 23:11:21 +02:00
}
2016-05-29 17:30:45 +02:00
2015-09-03 12:25:21 +02:00
// IssueFields represents single fields of a JIRA issue.
// Every JIRA issue has several fields attached.
type IssueFields struct {
// TODO Missing fields
// * "timespent": null,
// * "aggregatetimespent": null,
// * "workratio": -1,
// * "lastViewed": null,
// * "timeestimate": null,
// * "aggregatetimeoriginalestimate": null,
// * "timeoriginalestimate": null,
// * "timetracking": {},
// * "aggregatetimeestimate": null,
// * "environment": null,
// * "duedate": null,
2016-03-27 14:03:40 +02:00
Type IssueType ` json:"issuetype" `
Project Project ` json:"project,omitempty" `
Resolution * Resolution ` json:"resolution,omitempty" `
Priority * Priority ` json:"priority,omitempty" `
Resolutiondate string ` json:"resolutiondate,omitempty" `
Created string ` json:"created,omitempty" `
Watches * Watches ` json:"watches,omitempty" `
2016-06-03 23:14:27 +02:00
Assignee * User ` json:"assignee,omitempty" `
2016-04-06 12:45:37 +02:00
Updated string ` json:"updated,omitempty" `
2016-03-27 14:03:40 +02:00
Description string ` json:"description,omitempty" `
Summary string ` json:"summary" `
2016-06-03 23:14:27 +02:00
Creator * User ` json:"Creator,omitempty" `
Reporter * User ` json:"reporter,omitempty" `
2016-03-27 14:03:40 +02:00
Components [ ] * Component ` json:"components,omitempty" `
Status * Status ` json:"status,omitempty" `
Progress * Progress ` json:"progress,omitempty" `
AggregateProgress * Progress ` json:"aggregateprogress,omitempty" `
2016-05-29 18:42:38 +02:00
Worklog * Worklog ` json:"worklog,omitempty" `
2016-03-27 14:03:40 +02:00
IssueLinks [ ] * IssueLink ` json:"issuelinks,omitempty" `
2016-04-06 12:45:37 +02:00
Comments [ ] * Comment ` json:"comment.comments,omitempty" `
2016-02-14 05:15:32 +02:00
FixVersions [ ] * FixVersion ` json:"fixVersions,omitempty" `
2016-05-29 17:30:45 +02:00
Labels [ ] string ` json:"labels,omitempty" `
2016-05-29 18:42:38 +02:00
Subtasks [ ] * Subtasks ` json:"subtasks,omitempty" `
2016-05-19 23:11:21 +02:00
Attachments [ ] * Attachment ` json:"attachment,omitempty" `
2015-09-03 12:25:21 +02:00
}
// IssueType represents a type of a JIRA issue.
// Typical types are "Request", "Bug", "Story", ...
type IssueType struct {
Self string ` json:"self,omitempty" `
ID string ` json:"id,omitempty" `
Description string ` json:"description,omitempty" `
IconURL string ` json:"iconUrl,omitempty" `
Name string ` json:"name,omitempty" `
Subtask bool ` json:"subtask,omitempty" `
2016-06-03 23:14:27 +02:00
AvatarID int ` json:"avatarId,omitempty" `
2015-09-03 12:25:21 +02:00
}
// Resolution represents a resolution of a JIRA issue.
// Typical types are "Fixed", "Suspended", "Won't Fix", ...
type Resolution struct {
Self string ` json:"self" `
ID string ` json:"id" `
Description string ` json:"description" `
Name string ` json:"name" `
}
// Priority represents a priority of a JIRA issue.
// Typical types are "Normal", "Moderate", "Urgent", ...
type Priority struct {
Self string ` json:"self,omitempty" `
IconURL string ` json:"iconUrl,omitempty" `
Name string ` json:"name,omitempty" `
ID string ` json:"id,omitempty" `
}
// Watches represents a type of how many user are "observing" a JIRA issue to track the status / updates.
type Watches struct {
Self string ` json:"self,omitempty" `
WatchCount int ` json:"watchCount,omitempty" `
IsWatching bool ` json:"isWatching,omitempty" `
}
2016-06-03 23:25:18 +02:00
// User represents a user who is this JIRA issue assigned to.
2016-06-03 23:14:27 +02:00
type User struct {
Self string ` json:"self,omitempty" `
Name string ` json:"name,omitempty" `
Key string ` json:"key,omitempty" `
EmailAddress string ` json:"emailAddress,omitempty" `
AvatarUrls AvatarUrls ` json:"avatarUrls,omitempty" `
DisplayName string ` json:"displayName,omitempty" `
Active bool ` json:"active,omitempty" `
TimeZone string ` json:"timeZone,omitempty" `
}
2016-06-03 23:25:18 +02:00
// AvatarUrls represents different dimensions of avatars / images
2016-06-03 23:14:27 +02:00
type AvatarUrls struct {
Four8X48 string ` json:"48x48,omitempty" `
Two4X24 string ` json:"24x24,omitempty" `
One6X16 string ` json:"16x16,omitempty" `
Three2X32 string ` json:"32x32,omitempty" `
2015-09-03 12:25:21 +02:00
}
// Component represents a "component" of a JIRA issue.
// Components can be user defined in every JIRA instance.
type Component struct {
Self string ` json:"self,omitempty" `
ID string ` json:"id,omitempty" `
Name string ` json:"name,omitempty" `
}
// Status represents the current status of a JIRA issue.
// Typical status are "Open", "In Progress", "Closed", ...
// Status can be user defined in every JIRA instance.
type Status struct {
Self string ` json:"self" `
Description string ` json:"description" `
IconURL string ` json:"iconUrl" `
Name string ` json:"name" `
ID string ` json:"id" `
StatusCategory StatusCategory ` json:"statusCategory" `
}
// StatusCategory represents the category a status belongs to.
// Those categories can be user defined in every JIRA instance.
type StatusCategory struct {
Self string ` json:"self" `
ID int ` json:"id" `
Name string ` json:"name" `
Key string ` json:"key" `
ColorName string ` json:"colorName" `
}
// Progress represents the progress of a JIRA issue.
type Progress struct {
Progress int ` json:"progress" `
Total int ` json:"total" `
}
2016-06-04 10:41:34 +02:00
// Time represents the Time definition of JIRA as a time.Time of go
type Time time . Time
2016-05-29 21:10:19 +02:00
2016-06-04 10:41:34 +02:00
// UnmarshalJSON will transform the JIRA time into a time.Time
// during the transformation of the JIRA JSON response
func ( t * Time ) UnmarshalJSON ( b [ ] byte ) error {
2016-05-29 17:30:45 +02:00
ti , err := time . Parse ( "\"2006-01-02T15:04:05.999-0700\"" , string ( b ) )
if err != nil {
return err
}
2016-06-04 10:41:34 +02:00
* t = Time ( ti )
2016-05-29 17:30:45 +02:00
return nil
2015-09-03 12:25:21 +02:00
}
// Worklog represents the work log of a JIRA issue.
2016-06-03 23:25:18 +02:00
// One Worklog contains zero or n WorklogRecords
2015-09-03 12:25:21 +02:00
// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
type Worklog struct {
2016-06-03 23:14:27 +02:00
StartAt int ` json:"startAt" `
MaxResults int ` json:"maxResults" `
Total int ` json:"total" `
Worklogs [ ] WorklogRecord ` json:"worklogs" `
}
2016-06-03 23:25:18 +02:00
// WorklogRecord represents one entry of a Worklog
2016-06-03 23:14:27 +02:00
type WorklogRecord struct {
2016-06-04 10:41:34 +02:00
Self string ` json:"self" `
Author User ` json:"author" `
UpdateAuthor User ` json:"updateAuthor" `
Comment string ` json:"comment" `
Created Time ` json:"created" `
Updated Time ` json:"updated" `
2016-06-05 16:00:32 +02:00
Started Time ` json:"started" `
2016-06-04 10:41:34 +02:00
TimeSpent string ` json:"timeSpent" `
TimeSpentSeconds int ` json:"timeSpentSeconds" `
ID string ` json:"id" `
IssueID string ` json:"issueId" `
2016-05-29 18:42:38 +02:00
}
2016-06-03 23:25:18 +02:00
// Subtasks represents all issues of a parent issue.
2016-05-29 18:42:38 +02:00
type Subtasks struct {
2016-06-03 23:14:27 +02:00
ID string ` json:"id" `
Key string ` json:"key" `
Self string ` json:"self" `
Fields IssueFields ` json:"fields" `
2015-09-03 12:25:21 +02:00
}
// IssueLink represents a link between two issues in JIRA.
type IssueLink struct {
2016-06-14 16:19:14 +02:00
ID string ` json:"id,omitempty" `
Self string ` json:"self,omitempty" `
2015-09-03 12:25:21 +02:00
Type IssueLinkType ` json:"type" `
2016-06-07 22:07:28 +02:00
OutwardIssue * Issue ` json:"outwardIssue" `
InwardIssue * Issue ` json:"inwardIssue" `
2016-06-14 16:19:14 +02:00
Comment * Comment ` json:"comment,omitempty" `
2015-09-03 12:25:21 +02:00
}
// IssueLinkType represents a type of a link between to issues in JIRA.
// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
type IssueLinkType struct {
2016-06-14 16:19:14 +02:00
ID string ` json:"id,omitempty" `
Self string ` json:"self,omitempty" `
2015-09-03 12:25:21 +02:00
Name string ` json:"name" `
Inward string ` json:"inward" `
Outward string ` json:"outward" `
}
// Comment represents a comment by a person to an issue in JIRA.
type Comment struct {
2016-06-07 22:07:44 +02:00
ID string ` json:"id,omitempty" `
2016-05-19 23:11:21 +02:00
Self string ` json:"self,omitempty" `
Name string ` json:"name,omitempty" `
2016-06-03 23:14:27 +02:00
Author User ` json:"author,omitempty" `
2016-05-19 23:11:21 +02:00
Body string ` json:"body,omitempty" `
2016-06-03 23:14:27 +02:00
UpdateAuthor User ` json:"updateAuthor,omitempty" `
2016-05-19 23:11:21 +02:00
Updated string ` json:"updated,omitempty" `
Created string ` json:"created,omitempty" `
Visibility CommentVisibility ` json:"visibility,omitempty" `
2015-09-03 12:25:21 +02:00
}
2016-02-14 05:15:32 +02:00
// FixVersion represents a software release in which an issue is fixed.
type FixVersion struct {
Archived * bool ` json:"archived,omitempty" `
ID string ` json:"id,omitempty" `
Name string ` json:"name,omitempty" `
ProjectID int ` json:"projectId,omitempty" `
ReleaseDate string ` json:"releaseDate,omitempty" `
Released * bool ` json:"released,omitempty" `
Self string ` json:"self,omitempty" `
UserReleaseDate string ` json:"userReleaseDate,omitempty" `
}
2016-03-27 14:03:40 +02:00
// CommentVisibility represents he visibility of a comment.
// E.g. Type could be "role" and Value "Administrators"
type CommentVisibility struct {
2016-05-19 23:11:21 +02:00
Type string ` json:"type,omitempty" `
Value string ` json:"value,omitempty" `
2016-05-29 17:30:45 +02:00
}
2016-06-04 10:41:34 +02:00
// searchResult is only a small wrapper arround the Search (with JQL) method
// to be able to parse the results
type searchResult struct {
Issues [ ] Issue ` json:"issues" `
}
// CustomFields represents custom fields of JIRA
// This can heavily differ between JIRA instances
type CustomFields map [ string ] string
2015-09-03 12:25:21 +02:00
// Get returns a full representation of the issue for the given issue key.
// JIRA will attempt to identify the issue by the issueIdOrKey path parameter.
// This can be an issue id, or an issue key.
// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved.
//
2016-03-27 14:24:48 +02:00
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue
2015-09-03 12:25:21 +02:00
func ( s * IssueService ) Get ( issueID string ) ( * Issue , * http . Response , error ) {
apiEndpoint := fmt . Sprintf ( "rest/api/2/issue/%s" , issueID )
req , err := s . client . NewRequest ( "GET" , apiEndpoint , nil )
if err != nil {
return nil , nil , err
}
issue := new ( Issue )
resp , err := s . client . Do ( req , issue )
if err != nil {
return nil , resp , err
}
return issue , resp , nil
}
2016-05-27 14:14:09 +02:00
// DownloadAttachment returns a http.Response of an attachment for a given attachmentID.
// The attachment is in the http.Response.Body of the response.
// This is an io.ReadCloser.
// The caller should close the resp.Body.
2016-05-19 23:11:21 +02:00
func ( s * IssueService ) DownloadAttachment ( attachmentID string ) ( * http . Response , error ) {
apiEndpoint := fmt . Sprintf ( "secure/attachment/%s/" , attachmentID )
2016-05-29 17:30:45 +02:00
req , err := s . client . NewRequest ( "GET" , apiEndpoint , nil )
if err != nil {
2016-05-19 23:11:21 +02:00
return nil , err
2016-05-29 17:30:45 +02:00
}
2016-05-27 14:14:09 +02:00
resp , err := s . client . Do ( req , nil )
2016-05-29 17:30:45 +02:00
if err != nil {
2016-05-19 23:11:21 +02:00
return resp , err
2016-05-29 17:30:45 +02:00
}
2016-05-19 23:11:21 +02:00
return resp , nil
}
2016-05-27 14:14:09 +02:00
// PostAttachment uploads r (io.Reader) as an attachment to a given attachmentID
2016-05-25 09:04:23 +02:00
func ( s * IssueService ) PostAttachment ( attachmentID string , r io . Reader , attachmentName string ) ( * [ ] Attachment , * http . Response , error ) {
2016-05-19 23:11:21 +02:00
apiEndpoint := fmt . Sprintf ( "rest/api/2/issue/%s/attachments" , attachmentID )
b := new ( bytes . Buffer )
writer := multipart . NewWriter ( b )
fw , err := writer . CreateFormFile ( "file" , attachmentName )
if err != nil {
return nil , nil , err
2016-05-29 17:30:45 +02:00
}
2016-05-25 09:47:10 +02:00
if r != nil {
// Copy the file
if _ , err = io . Copy ( fw , r ) ; err != nil {
return nil , nil , err
2016-05-29 17:30:45 +02:00
}
}
2016-05-19 23:11:21 +02:00
writer . Close ( )
req , err := s . client . NewMultiPartRequest ( "POST" , apiEndpoint , b )
if err != nil {
return nil , nil , err
}
req . Header . Set ( "Content-Type" , writer . FormDataContentType ( ) )
2016-05-25 09:04:23 +02:00
// PostAttachment response returns a JSON array (as multiple attachments can be posted)
attachment := new ( [ ] Attachment )
resp , err := s . client . Do ( req , attachment )
2016-05-19 23:11:21 +02:00
if err != nil {
return nil , resp , err
}
2016-05-25 09:04:23 +02:00
return attachment , resp , nil
2016-05-29 17:30:45 +02:00
}
2015-09-03 12:25:21 +02:00
// Create creates an issue or a sub-task from a JSON representation.
// Creating a sub-task is similar to creating a regular issue, with two important differences:
// The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue.
//
2016-03-27 14:24:48 +02:00
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues
2015-09-03 12:25:21 +02:00
func ( s * IssueService ) Create ( issue * Issue ) ( * Issue , * http . Response , error ) {
apiEndpoint := "rest/api/2/issue/"
req , err := s . client . NewRequest ( "POST" , apiEndpoint , issue )
if err != nil {
return nil , nil , err
}
responseIssue := new ( Issue )
resp , err := s . client . Do ( req , responseIssue )
if err != nil {
return nil , resp , err
}
return responseIssue , resp , nil
}
2016-01-05 16:22:21 +02:00
2016-03-27 14:03:40 +02:00
// AddComment adds a new comment to issueID.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addComment
func ( s * IssueService ) AddComment ( issueID string , comment * Comment ) ( * Comment , * http . Response , error ) {
2016-01-05 16:22:21 +02:00
apiEndpoint := fmt . Sprintf ( "rest/api/2/issue/%s/comment" , issueID )
2016-03-27 14:03:40 +02:00
req , err := s . client . NewRequest ( "POST" , apiEndpoint , comment )
2016-01-05 16:22:21 +02:00
if err != nil {
return nil , nil , err
}
2016-03-27 14:03:40 +02:00
responseComment := new ( Comment )
2016-02-29 09:29:44 +02:00
resp , err := s . client . Do ( req , responseComment )
if err != nil {
return nil , resp , err
}
2016-01-05 16:22:21 +02:00
2016-03-27 14:03:40 +02:00
return responseComment , resp , nil
}
2016-02-29 09:29:44 +02:00
// AddLink adds a link between two issues.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink
func ( s * IssueService ) AddLink ( issueLink * IssueLink ) ( * http . Response , error ) {
apiEndpoint := fmt . Sprintf ( "rest/api/2/issueLink" )
req , err := s . client . NewRequest ( "POST" , apiEndpoint , issueLink )
if err != nil {
return nil , err
}
resp , err := s . client . Do ( req , nil )
return resp , err
}
2016-05-29 17:30:45 +02:00
2016-06-04 10:41:34 +02:00
// Search will search for tickets according to the jql
//
2016-05-29 17:30:45 +02:00
// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues
2016-06-04 10:41:34 +02:00
func ( s * IssueService ) Search ( jql string ) ( [ ] Issue , * http . Response , error ) {
u := fmt . Sprintf ( "rest/api/2/search?jql=%s" , url . QueryEscape ( jql ) )
req , err := s . client . NewRequest ( "GET" , u , nil )
2016-05-29 17:30:45 +02:00
if err != nil {
2016-06-04 10:41:34 +02:00
return [ ] Issue { } , nil , err
2016-05-29 17:30:45 +02:00
}
2016-06-04 02:51:44 +02:00
2016-06-04 10:41:34 +02:00
v := new ( searchResult )
resp , err := s . client . Do ( req , v )
return v . Issues , resp , err
}
2016-06-04 02:51:44 +02:00
2016-06-04 10:41:34 +02:00
// GetCustomFields returns a map of customfield_* keys with string values
2016-06-04 02:51:44 +02:00
func ( s * IssueService ) GetCustomFields ( issueID string ) ( CustomFields , * http . Response , error ) {
apiEndpoint := fmt . Sprintf ( "rest/api/2/issue/%s" , issueID )
req , err := s . client . NewRequest ( "GET" , apiEndpoint , nil )
if err != nil {
return nil , nil , err
}
issue := new ( map [ string ] interface { } )
resp , err := s . client . Do ( req , issue )
if err != nil {
return nil , resp , err
}
2016-06-04 10:41:34 +02:00
2016-06-04 02:51:44 +02:00
m := * issue
f := m [ "fields" ]
cf := make ( CustomFields )
if f == nil {
return cf , resp , nil
}
if rec , ok := f . ( map [ string ] interface { } ) ; ok {
for key , val := range rec {
if strings . Contains ( key , "customfield" ) {
cf [ key ] = fmt . Sprint ( val )
}
}
}
return cf , resp , nil
2016-05-29 17:30:45 +02:00
}