You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

574 lines
19 KiB

  1. package jira
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "mime/multipart"
  7. "net/url"
  8. "strings"
  9. "time"
  10. )
  11. const (
  12. // AssigneeAutomatic represents the value of the "Assignee: Automatic" of JIRA
  13. AssigneeAutomatic = "-1"
  14. )
  15. // IssueService handles Issues for the JIRA instance / API.
  16. //
  17. // JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue
  18. type IssueService struct {
  19. client *Client
  20. }
  21. // Issue represents a JIRA issue.
  22. type Issue struct {
  23. Expand string `json:"expand,omitempty"`
  24. ID string `json:"id,omitempty"`
  25. Self string `json:"self,omitempty"`
  26. Key string `json:"key,omitempty"`
  27. Fields *IssueFields `json:"fields,omitempty"`
  28. }
  29. // Attachment represents a JIRA attachment
  30. type Attachment struct {
  31. Self string `json:"self,omitempty"`
  32. ID string `json:"id,omitempty"`
  33. Filename string `json:"filename,omitempty"`
  34. Author *User `json:"author,omitempty"`
  35. Created string `json:"created,omitempty"`
  36. Size int `json:"size,omitempty"`
  37. MimeType string `json:"mimeType,omitempty"`
  38. Content string `json:"content,omitempty"`
  39. Thumbnail string `json:"thumbnail,omitempty"`
  40. }
  41. // Epic represents the epic to which an issue is associated
  42. // Not that this struct does not process the returned "color" value
  43. type Epic struct {
  44. ID int `json:"id"`
  45. Key string `json:"key"`
  46. Self string `json:"self"`
  47. Name string `json:"name"`
  48. Summary string `json:"summary"`
  49. Done bool `json:"done"`
  50. }
  51. // IssueFields represents single fields of a JIRA issue.
  52. // Every JIRA issue has several fields attached.
  53. type IssueFields struct {
  54. // TODO Missing fields
  55. // * "timespent": null,
  56. // * "aggregatetimespent": null,
  57. // * "workratio": -1,
  58. // * "lastViewed": null,
  59. // * "timeestimate": null,
  60. // * "aggregatetimeoriginalestimate": null,
  61. // * "timeoriginalestimate": null,
  62. // * "timetracking": {},
  63. // * "aggregatetimeestimate": null,
  64. // * "environment": null,
  65. // * "duedate": null,
  66. Type IssueType `json:"issuetype"`
  67. Project Project `json:"project,omitempty"`
  68. Resolution *Resolution `json:"resolution,omitempty"`
  69. Priority *Priority `json:"priority,omitempty"`
  70. Resolutiondate string `json:"resolutiondate,omitempty"`
  71. Created string `json:"created,omitempty"`
  72. Watches *Watches `json:"watches,omitempty"`
  73. Assignee *User `json:"assignee,omitempty"`
  74. Updated string `json:"updated,omitempty"`
  75. Description string `json:"description,omitempty"`
  76. Summary string `json:"summary"`
  77. Creator *User `json:"Creator,omitempty"`
  78. Reporter *User `json:"reporter,omitempty"`
  79. Components []*Component `json:"components,omitempty"`
  80. Status *Status `json:"status,omitempty"`
  81. Progress *Progress `json:"progress,omitempty"`
  82. AggregateProgress *Progress `json:"aggregateprogress,omitempty"`
  83. Worklog *Worklog `json:"worklog,omitempty"`
  84. IssueLinks []*IssueLink `json:"issuelinks,omitempty"`
  85. Comments *Comments `json:"comment,omitempty"`
  86. FixVersions []*FixVersion `json:"fixVersions,omitempty"`
  87. Labels []string `json:"labels,omitempty"`
  88. Subtasks []*Subtasks `json:"subtasks,omitempty"`
  89. Attachments []*Attachment `json:"attachment,omitempty"`
  90. Epic *Epic `json:"epic,omitempty"`
  91. }
  92. // IssueType represents a type of a JIRA issue.
  93. // Typical types are "Request", "Bug", "Story", ...
  94. type IssueType struct {
  95. Self string `json:"self,omitempty"`
  96. ID string `json:"id,omitempty"`
  97. Description string `json:"description,omitempty"`
  98. IconURL string `json:"iconUrl,omitempty"`
  99. Name string `json:"name,omitempty"`
  100. Subtask bool `json:"subtask,omitempty"`
  101. AvatarID int `json:"avatarId,omitempty"`
  102. }
  103. // Resolution represents a resolution of a JIRA issue.
  104. // Typical types are "Fixed", "Suspended", "Won't Fix", ...
  105. type Resolution struct {
  106. Self string `json:"self"`
  107. ID string `json:"id"`
  108. Description string `json:"description"`
  109. Name string `json:"name"`
  110. }
  111. // Priority represents a priority of a JIRA issue.
  112. // Typical types are "Normal", "Moderate", "Urgent", ...
  113. type Priority struct {
  114. Self string `json:"self,omitempty"`
  115. IconURL string `json:"iconUrl,omitempty"`
  116. Name string `json:"name,omitempty"`
  117. ID string `json:"id,omitempty"`
  118. }
  119. // Watches represents a type of how many user are "observing" a JIRA issue to track the status / updates.
  120. type Watches struct {
  121. Self string `json:"self,omitempty"`
  122. WatchCount int `json:"watchCount,omitempty"`
  123. IsWatching bool `json:"isWatching,omitempty"`
  124. }
  125. // User represents a user who is this JIRA issue assigned to.
  126. type User struct {
  127. Self string `json:"self,omitempty"`
  128. Name string `json:"name,omitempty"`
  129. Key string `json:"key,omitempty"`
  130. EmailAddress string `json:"emailAddress,omitempty"`
  131. AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"`
  132. DisplayName string `json:"displayName,omitempty"`
  133. Active bool `json:"active,omitempty"`
  134. TimeZone string `json:"timeZone,omitempty"`
  135. }
  136. // AvatarUrls represents different dimensions of avatars / images
  137. type AvatarUrls struct {
  138. Four8X48 string `json:"48x48,omitempty"`
  139. Two4X24 string `json:"24x24,omitempty"`
  140. One6X16 string `json:"16x16,omitempty"`
  141. Three2X32 string `json:"32x32,omitempty"`
  142. }
  143. // Component represents a "component" of a JIRA issue.
  144. // Components can be user defined in every JIRA instance.
  145. type Component struct {
  146. Self string `json:"self,omitempty"`
  147. ID string `json:"id,omitempty"`
  148. Name string `json:"name,omitempty"`
  149. }
  150. // Status represents the current status of a JIRA issue.
  151. // Typical status are "Open", "In Progress", "Closed", ...
  152. // Status can be user defined in every JIRA instance.
  153. type Status struct {
  154. Self string `json:"self"`
  155. Description string `json:"description"`
  156. IconURL string `json:"iconUrl"`
  157. Name string `json:"name"`
  158. ID string `json:"id"`
  159. StatusCategory StatusCategory `json:"statusCategory"`
  160. }
  161. // StatusCategory represents the category a status belongs to.
  162. // Those categories can be user defined in every JIRA instance.
  163. type StatusCategory struct {
  164. Self string `json:"self"`
  165. ID int `json:"id"`
  166. Name string `json:"name"`
  167. Key string `json:"key"`
  168. ColorName string `json:"colorName"`
  169. }
  170. // Progress represents the progress of a JIRA issue.
  171. type Progress struct {
  172. Progress int `json:"progress"`
  173. Total int `json:"total"`
  174. }
  175. // Time represents the Time definition of JIRA as a time.Time of go
  176. type Time time.Time
  177. // Wrapper struct for search result
  178. type transitionResult struct {
  179. Transitions []Transition `json:"transitions"`
  180. }
  181. // Transition represents an issue transition in JIRA
  182. type Transition struct {
  183. ID string `json:"id"`
  184. Name string `json:"name"`
  185. Fields map[string]TransitionField `json:"fields"`
  186. }
  187. // TransitionField represents the value of one Transistion
  188. type TransitionField struct {
  189. Required bool `json:"required"`
  190. }
  191. // CreateTransitionPayload is used for creating new issue transitions
  192. type CreateTransitionPayload struct {
  193. Transition TransitionPayload `json:"transition"`
  194. }
  195. // TransitionPayload represents the request payload of Transistion calls like DoTransition
  196. type TransitionPayload struct {
  197. ID string `json:"id"`
  198. }
  199. // UnmarshalJSON will transform the JIRA time into a time.Time
  200. // during the transformation of the JIRA JSON response
  201. func (t *Time) UnmarshalJSON(b []byte) error {
  202. ti, err := time.Parse("\"2006-01-02T15:04:05.999-0700\"", string(b))
  203. if err != nil {
  204. return err
  205. }
  206. *t = Time(ti)
  207. return nil
  208. }
  209. // Worklog represents the work log of a JIRA issue.
  210. // One Worklog contains zero or n WorklogRecords
  211. // JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
  212. type Worklog struct {
  213. StartAt int `json:"startAt"`
  214. MaxResults int `json:"maxResults"`
  215. Total int `json:"total"`
  216. Worklogs []WorklogRecord `json:"worklogs"`
  217. }
  218. // WorklogRecord represents one entry of a Worklog
  219. type WorklogRecord struct {
  220. Self string `json:"self"`
  221. Author User `json:"author"`
  222. UpdateAuthor User `json:"updateAuthor"`
  223. Comment string `json:"comment"`
  224. Created Time `json:"created"`
  225. Updated Time `json:"updated"`
  226. Started Time `json:"started"`
  227. TimeSpent string `json:"timeSpent"`
  228. TimeSpentSeconds int `json:"timeSpentSeconds"`
  229. ID string `json:"id"`
  230. IssueID string `json:"issueId"`
  231. }
  232. // Subtasks represents all issues of a parent issue.
  233. type Subtasks struct {
  234. ID string `json:"id"`
  235. Key string `json:"key"`
  236. Self string `json:"self"`
  237. Fields IssueFields `json:"fields"`
  238. }
  239. // IssueLink represents a link between two issues in JIRA.
  240. type IssueLink struct {
  241. ID string `json:"id,omitempty"`
  242. Self string `json:"self,omitempty"`
  243. Type IssueLinkType `json:"type"`
  244. OutwardIssue *Issue `json:"outwardIssue"`
  245. InwardIssue *Issue `json:"inwardIssue"`
  246. Comment *Comment `json:"comment,omitempty"`
  247. }
  248. // IssueLinkType represents a type of a link between to issues in JIRA.
  249. // Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
  250. type IssueLinkType struct {
  251. ID string `json:"id,omitempty"`
  252. Self string `json:"self,omitempty"`
  253. Name string `json:"name"`
  254. Inward string `json:"inward"`
  255. Outward string `json:"outward"`
  256. }
  257. // Comments represents a list of Comment.
  258. type Comments struct {
  259. Comments []*Comment `json:"comments,omitempty"`
  260. }
  261. // Comment represents a comment by a person to an issue in JIRA.
  262. type Comment struct {
  263. ID string `json:"id,omitempty"`
  264. Self string `json:"self,omitempty"`
  265. Name string `json:"name,omitempty"`
  266. Author User `json:"author,omitempty"`
  267. Body string `json:"body,omitempty"`
  268. UpdateAuthor User `json:"updateAuthor,omitempty"`
  269. Updated string `json:"updated,omitempty"`
  270. Created string `json:"created,omitempty"`
  271. Visibility CommentVisibility `json:"visibility,omitempty"`
  272. }
  273. // FixVersion represents a software release in which an issue is fixed.
  274. type FixVersion struct {
  275. Archived *bool `json:"archived,omitempty"`
  276. ID string `json:"id,omitempty"`
  277. Name string `json:"name,omitempty"`
  278. ProjectID int `json:"projectId,omitempty"`
  279. ReleaseDate string `json:"releaseDate,omitempty"`
  280. Released *bool `json:"released,omitempty"`
  281. Self string `json:"self,omitempty"`
  282. UserReleaseDate string `json:"userReleaseDate,omitempty"`
  283. }
  284. // CommentVisibility represents he visibility of a comment.
  285. // E.g. Type could be "role" and Value "Administrators"
  286. type CommentVisibility struct {
  287. Type string `json:"type,omitempty"`
  288. Value string `json:"value,omitempty"`
  289. }
  290. // SearchOptions specifies the optional parameters to various List methods that
  291. // support pagination.
  292. // Pagination is used for the JIRA REST APIs to conserve server resources and limit
  293. // response size for resources that return potentially large collection of items.
  294. // A request to a pages API will result in a values array wrapped in a JSON object with some paging metadata
  295. // Default Pagination options
  296. type SearchOptions struct {
  297. // StartAt: The starting index of the returned projects. Base index: 0.
  298. StartAt int `url:"startAt,omitempty"`
  299. // MaxResults: The maximum number of projects to return per page. Default: 50.
  300. MaxResults int `url:"maxResults,omitempty"`
  301. }
  302. // searchResult is only a small wrapper arround the Search (with JQL) method
  303. // to be able to parse the results
  304. type searchResult struct {
  305. Issues []Issue `json:"issues"`
  306. StartAt int `json:"startAt"`
  307. MaxResults int `json:"maxResults"`
  308. Total int `json:"total"`
  309. }
  310. // CustomFields represents custom fields of JIRA
  311. // This can heavily differ between JIRA instances
  312. type CustomFields map[string]string
  313. // Get returns a full representation of the issue for the given issue key.
  314. // JIRA will attempt to identify the issue by the issueIdOrKey path parameter.
  315. // This can be an issue id, or an issue key.
  316. // 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.
  317. //
  318. // JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue
  319. func (s *IssueService) Get(issueID string) (*Issue, *Response, error) {
  320. apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
  321. req, err := s.client.NewRequest("GET", apiEndpoint, nil)
  322. if err != nil {
  323. return nil, nil, err
  324. }
  325. issue := new(Issue)
  326. resp, err := s.client.Do(req, issue)
  327. if err != nil {
  328. return nil, resp, err
  329. }
  330. return issue, resp, nil
  331. }
  332. // DownloadAttachment returns a Response of an attachment for a given attachmentID.
  333. // The attachment is in the Response.Body of the response.
  334. // This is an io.ReadCloser.
  335. // The caller should close the resp.Body.
  336. func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error) {
  337. apiEndpoint := fmt.Sprintf("secure/attachment/%s/", attachmentID)
  338. req, err := s.client.NewRequest("GET", apiEndpoint, nil)
  339. if err != nil {
  340. return nil, err
  341. }
  342. resp, err := s.client.Do(req, nil)
  343. if err != nil {
  344. return resp, err
  345. }
  346. return resp, nil
  347. }
  348. // PostAttachment uploads r (io.Reader) as an attachment to a given attachmentID
  349. func (s *IssueService) PostAttachment(attachmentID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) {
  350. apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", attachmentID)
  351. b := new(bytes.Buffer)
  352. writer := multipart.NewWriter(b)
  353. fw, err := writer.CreateFormFile("file", attachmentName)
  354. if err != nil {
  355. return nil, nil, err
  356. }
  357. if r != nil {
  358. // Copy the file
  359. if _, err = io.Copy(fw, r); err != nil {
  360. return nil, nil, err
  361. }
  362. }
  363. writer.Close()
  364. req, err := s.client.NewMultiPartRequest("POST", apiEndpoint, b)
  365. if err != nil {
  366. return nil, nil, err
  367. }
  368. req.Header.Set("Content-Type", writer.FormDataContentType())
  369. // PostAttachment response returns a JSON array (as multiple attachments can be posted)
  370. attachment := new([]Attachment)
  371. resp, err := s.client.Do(req, attachment)
  372. if err != nil {
  373. return nil, resp, err
  374. }
  375. return attachment, resp, nil
  376. }
  377. // Create creates an issue or a sub-task from a JSON representation.
  378. // Creating a sub-task is similar to creating a regular issue, with two important differences:
  379. // 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.
  380. //
  381. // JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues
  382. func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) {
  383. apiEndpoint := "rest/api/2/issue/"
  384. req, err := s.client.NewRequest("POST", apiEndpoint, issue)
  385. if err != nil {
  386. return nil, nil, err
  387. }
  388. responseIssue := new(Issue)
  389. resp, err := s.client.Do(req, responseIssue)
  390. if err != nil {
  391. return nil, resp, err
  392. }
  393. return responseIssue, resp, nil
  394. }
  395. // AddComment adds a new comment to issueID.
  396. //
  397. // JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addComment
  398. func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *Response, error) {
  399. apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment", issueID)
  400. req, err := s.client.NewRequest("POST", apiEndpoint, comment)
  401. if err != nil {
  402. return nil, nil, err
  403. }
  404. responseComment := new(Comment)
  405. resp, err := s.client.Do(req, responseComment)
  406. if err != nil {
  407. return nil, resp, err
  408. }
  409. return responseComment, resp, nil
  410. }
  411. // AddLink adds a link between two issues.
  412. //
  413. // JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink
  414. func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) {
  415. apiEndpoint := fmt.Sprintf("rest/api/2/issueLink")
  416. req, err := s.client.NewRequest("POST", apiEndpoint, issueLink)
  417. if err != nil {
  418. return nil, err
  419. }
  420. resp, err := s.client.Do(req, nil)
  421. return resp, err
  422. }
  423. // Search will search for tickets according to the jql
  424. //
  425. // JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues
  426. func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Response, error) {
  427. var u string
  428. if options == nil {
  429. u = fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql))
  430. } else {
  431. u = fmt.Sprintf("rest/api/2/search?jql=%s&startAt=%d&maxResults=%d", url.QueryEscape(jql),
  432. options.StartAt, options.MaxResults)
  433. }
  434. req, err := s.client.NewRequest("GET", u, nil)
  435. if err != nil {
  436. return []Issue{}, nil, err
  437. }
  438. v := new(searchResult)
  439. resp, err := s.client.Do(req, v)
  440. return v.Issues, resp, err
  441. }
  442. // GetCustomFields returns a map of customfield_* keys with string values
  443. func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) {
  444. apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
  445. req, err := s.client.NewRequest("GET", apiEndpoint, nil)
  446. if err != nil {
  447. return nil, nil, err
  448. }
  449. issue := new(map[string]interface{})
  450. resp, err := s.client.Do(req, issue)
  451. if err != nil {
  452. return nil, resp, err
  453. }
  454. m := *issue
  455. f := m["fields"]
  456. cf := make(CustomFields)
  457. if f == nil {
  458. return cf, resp, nil
  459. }
  460. if rec, ok := f.(map[string]interface{}); ok {
  461. for key, val := range rec {
  462. if strings.Contains(key, "customfield") {
  463. cf[key] = fmt.Sprint(val)
  464. }
  465. }
  466. }
  467. return cf, resp, nil
  468. }
  469. // GetTransitions gets a list of the transitions possible for this issue by the current user,
  470. // along with fields that are required and their types.
  471. //
  472. // JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getTransitions
  473. func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error) {
  474. apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions?expand=transitions.fields", id)
  475. req, err := s.client.NewRequest("GET", apiEndpoint, nil)
  476. if err != nil {
  477. return nil, nil, err
  478. }
  479. result := new(transitionResult)
  480. resp, err := s.client.Do(req, result)
  481. return result.Transitions, resp, err
  482. }
  483. // DoTransition performs a transition on an issue.
  484. // When performing the transition you can update or set other issue fields.
  485. //
  486. // JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition
  487. func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) {
  488. apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions", ticketID)
  489. payload := CreateTransitionPayload{
  490. Transition: TransitionPayload{
  491. ID: transitionID,
  492. },
  493. }
  494. req, err := s.client.NewRequest("POST", apiEndpoint, payload)
  495. if err != nil {
  496. return nil, err
  497. }
  498. resp, err := s.client.Do(req, nil)
  499. if err != nil {
  500. return nil, err
  501. }
  502. return resp, nil
  503. }