Files
jira/commands.go
T
Mike Pountney e042a3e62a Add 'labels' command to set/add/remove labels
This adds 'labels' command, which allows for the setting, addition and removal
of labels on an issue.

'set' action resets the issue labels to the list provided.
'add' action adds the supplied labels to the issue
'remove' action removes the supplied labels from the issue

The API already gracefully handles duplication, removal of non-existant labels, and the
supplying of an empty list of labels (which is useful in the case of 'set')

Eg

jira labels TEST-123 add label1 label2 label3
jira labels TEST-123 remove label1 label2
jira labels TEST-123 set label1 label2 label3
jira labels TEST-123 set # clears any labels on the issue
jira labels TEST-123 add # no-op
jira labels TEST-123 remove # no-op

This mirrors the functionality of the API.
2015-12-24 11:30:52 -08:00

695 lines
18 KiB
Go

package jira
import (
"bytes"
"fmt"
"golang.org/x/crypto/ssh/terminal"
"net/http"
"net/http/httputil"
"os"
"strings"
// "github.com/kr/pretty"
)
func (c *Cli) CmdLogin() error {
uri := fmt.Sprintf("%s/rest/auth/1/session", c.endpoint)
for true {
req, _ := http.NewRequest("GET", uri, nil)
user, _ := c.opts["user"].(string)
fmt.Printf("Enter Password for %s: ", user)
pwbytes, _ := terminal.ReadPassword(int(os.Stdin.Fd()))
passwd := string(pwbytes)
req.SetBasicAuth(user, passwd)
log.Info("%s %s", req.Method, req.URL.String())
if resp, err := c.makeRequest(req); err != nil {
return err
} else {
out, _ := httputil.DumpResponse(resp, true)
log.Debug("%s", out)
if resp.StatusCode == 403 {
// probably got this, need to redirect the user to login manually
// X-Authentication-Denied-Reason: CAPTCHA_CHALLENGE; login-url=https://jira/login.jsp
if reason := resp.Header.Get("X-Authentication-Denied-Reason"); reason != "" {
err := fmt.Errorf("Authenticaion Failed: %s", reason)
log.Error("%s", err)
return err
}
err := fmt.Errorf("Authentication Failed: Unknown Reason")
log.Error("%s", err)
return err
} else if resp.StatusCode == 200 {
// https://confluence.atlassian.com/display/JIRA043/JIRA+REST+API+%28Alpha%29+Tutorial#JIRARESTAPI%28Alpha%29Tutorial-CAPTCHAs
// probably bad password, try again
if reason := resp.Header.Get("X-Seraph-Loginreason"); reason == "AUTHENTICATION_DENIED" {
log.Warning("Authentication Failed: %s", reason)
continue
}
} else {
log.Warning("Login failed")
continue
}
}
return nil
}
return nil
}
func (c *Cli) CmdFields() error {
log.Debug("fields called")
uri := fmt.Sprintf("%s/rest/api/2/field", c.endpoint)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("fields"), data, nil)
}
func (c *Cli) CmdList() error {
log.Debug("list called")
if data, err := c.FindIssues(); err != nil {
return err
} else {
return runTemplate(c.getTemplate("list"), data, nil)
}
}
func (c *Cli) CmdView(issue string) error {
log.Debug("view called")
c.Browse(issue)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("view"), data, nil)
}
func (c *Cli) CmdEdit(issue string) error {
log.Debug("edit called")
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/editmeta", c.endpoint, issue)
editmeta, err := responseToJson(c.get(uri))
if err != nil {
return err
}
uri = fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
var issueData map[string]interface{}
if data, err := responseToJson(c.get(uri)); err != nil {
return err
} else {
issueData = data.(map[string]interface{})
}
issueData["meta"] = editmeta.(map[string]interface{})
issueData["overrides"] = c.opts
return c.editTemplate(
c.getTemplate("edit"),
fmt.Sprintf("%s-edit-", issue),
issueData,
func(json string) error {
if c.getOptBool("dryrun", false) {
log.Debug("PUT: %s", json)
log.Debug("Dryrun mode, skipping PUT")
return nil
}
resp, err := c.put(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 204 {
c.Browse(issueData["key"].(string))
if !c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issueData["key"], c.endpoint, issueData["key"])
}
return nil
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From PUT")
log.Error("%s:\n%s", err, logBuffer)
return err
}
},
)
}
func (c *Cli) CmdEditMeta(issue string) error {
log.Debug("editMeta called")
c.Browse(issue)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/editmeta", c.endpoint, issue)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("editmeta"), data, nil)
}
func (c *Cli) CmdTransitionMeta(issue string) error {
log.Debug("tranisionMeta called")
c.Browse(issue)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", c.endpoint, issue)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("transmeta"), data, nil)
}
func (c *Cli) CmdIssueTypes() error {
project := c.opts["project"].(string)
log.Debug("issueTypes called")
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s", c.endpoint, project)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("issuetypes"), data, nil)
}
func (c *Cli) CmdCreateMeta() error {
project := c.opts["project"].(string)
issuetype := c.getOptString("issuetype", "Bug")
log.Debug("createMeta called")
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", c.endpoint, project, issuetype)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
if val, ok := data.(map[string]interface{})["projects"]; ok {
if len(val.([]interface{})) == 0 {
err = fmt.Errorf("Project '%s' or issuetype '%s' unknown. Unable to createmeta.", project, issuetype)
log.Error("%s", err)
return err
}
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
data = val.([]interface{})[0]
}
}
return runTemplate(c.getTemplate("createmeta"), data, nil)
}
func (c *Cli) CmdTransitions(issue string) error {
log.Debug("Transitions called")
c.Browse(issue)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", c.endpoint, issue)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("transitions"), data, nil)
}
func (c *Cli) CmdCreate() error {
project := c.opts["project"].(string)
issuetype := c.getOptString("issuetype", "Bug")
log.Debug("create called")
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", c.endpoint, project, issuetype)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
issueData := make(map[string]interface{})
issueData["overrides"] = c.opts
issueData["overrides"].(map[string]interface{})["issuetype"] = issuetype
if val, ok := data.(map[string]interface{})["projects"]; ok {
if len(val.([]interface{})) == 0 {
err = fmt.Errorf("Project '%s' or issuetype '%s' unknown. Unable to create issue.", project, issuetype)
log.Error("%s", err)
return err
}
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
issueData["meta"] = val.([]interface{})[0]
}
}
sanitizedType := strings.ToLower(strings.Replace(issuetype, " ", "", -1))
return c.editTemplate(
c.getTemplate(fmt.Sprintf("create-%s", sanitizedType)),
fmt.Sprintf("create-%s-", sanitizedType),
issueData,
func(json string) error {
log.Debug("JSON: %s", json)
uri := fmt.Sprintf("%s/rest/api/2/issue", c.endpoint)
if c.getOptBool("dryrun", false) {
log.Debug("POST: %s", json)
log.Debug("Dryrun mode, skipping POST")
return nil
}
resp, err := c.post(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 201 {
// response: {"id":"410836","key":"PROJ-238","self":"https://jira/rest/api/2/issue/410836"}
if json, err := responseToJson(resp, nil); err != nil {
return err
} else {
key := json.(map[string]interface{})["key"].(string)
link := fmt.Sprintf("%s/browse/%s", c.endpoint, key)
c.Browse(key)
c.SaveData(map[string]string{
"issue": key,
"link": link,
})
if !c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s\n", key, link)
}
}
return nil
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
},
)
return nil
}
func (c *Cli) CmdIssueLinkTypes() error {
log.Debug("Transitions called")
uri := fmt.Sprintf("%s/rest/api/2/issueLinkType", c.endpoint)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("issuelinktypes"), data, nil)
}
func (c *Cli) CmdBlocks(blocker string, issue string) error {
log.Debug("blocks called")
json, err := jsonEncode(map[string]interface{}{
"type": map[string]string{
"name": "Depends", // TODO This is probably not constant across Jira installs
},
"inwardIssue": map[string]string{
"key": issue,
},
"outwardIssue": map[string]string{
"key": blocker,
},
})
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issueLink", c.endpoint)
if c.getOptBool("dryrun", false) {
log.Debug("POST: %s", json)
log.Debug("Dryrun mode, skipping POST")
return nil
}
resp, err := c.post(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 201 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
return nil
}
func (c *Cli) CmdDups(duplicate string, issue string) error {
log.Debug("dups called")
json, err := jsonEncode(map[string]interface{}{
"type": map[string]string{
"name": "Duplicate", // TODO This is probably not constant across Jira installs
},
"inwardIssue": map[string]string{
"key": duplicate,
},
"outwardIssue": map[string]string{
"key": issue,
},
})
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issueLink", c.endpoint)
if c.getOptBool("dryrun", false) {
log.Debug("POST: %s", json)
log.Debug("Dryrun mode, skipping POST")
return nil
}
resp, err := c.post(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 201 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
return nil
}
func (c *Cli) CmdWatch(issue string) error {
watcher := c.getOptString("watcher", c.opts["user"].(string))
log.Debug("watch called")
json, err := jsonEncode(watcher)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", c.endpoint, issue)
if c.getOptBool("dryrun", false) {
log.Debug("POST: %s", json)
log.Debug("Dryrun mode, skipping POST")
return nil
}
resp, err := c.post(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 204 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
return nil
}
func (c *Cli) CmdTransition(issue string, trans string) error {
log.Debug("transition called")
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", c.endpoint, issue)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
transitions := data.(map[string]interface{})["transitions"].([]interface{})
var transId, transName string
var transMeta map[string]interface{}
found := make([]string, 0, len(transitions))
for _, transition := range transitions {
name := transition.(map[string]interface{})["name"].(string)
id := transition.(map[string]interface{})["id"].(string)
found = append(found, name)
if strings.Contains(strings.ToLower(name), strings.ToLower(trans)) {
transName = name
transId = id
transMeta = transition.(map[string]interface{})
}
}
if transId == "" {
err := fmt.Errorf("Invalid Transition '%s', Available: %s", trans, strings.Join(found, ", "))
log.Error("%s", err)
return err
}
handlePost := func(json string) error {
log.Debug("POST: %s", json)
// os.Exit(0)
uri = fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", c.endpoint, issue)
if c.getOptBool("dryrun", false) {
log.Debug("POST: %s", json)
log.Debug("Dryrun mode, skipping POST")
return nil
}
resp, err := c.post(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 204 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
return nil
}
uri = fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
var issueData map[string]interface{}
if data, err := responseToJson(c.get(uri)); err != nil {
return err
} else {
issueData = data.(map[string]interface{})
}
issueData["meta"] = transMeta
issueData["overrides"] = c.opts
issueData["transition"] = map[string]interface{}{
"name": transName,
"id": transId,
}
return c.editTemplate(
c.getTemplate("transition"),
fmt.Sprintf("%s-trans-%s-", issue, trans),
issueData,
handlePost,
)
}
func (c *Cli) CmdComment(issue string) error {
log.Debug("comment called")
handlePost := func(json string) error {
log.Debug("JSON: %s", json)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/comment", c.endpoint, issue)
if c.getOptBool("dryrun", false) {
log.Debug("POST: %s", json)
log.Debug("Dryrun mode, skipping POST")
return nil
}
resp, err := c.post(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 201 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
return nil
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
}
if comment, ok := c.opts["comment"]; ok && comment != "" {
json, err := jsonEncode(map[string]interface{}{
"body": comment,
})
if err != nil {
return err
}
return handlePost(json)
} else {
return c.editTemplate(
c.getTemplate("comment"),
fmt.Sprintf("%s-create-", issue),
map[string]interface{}{},
handlePost,
)
}
return nil
}
func (c *Cli) CmdLabels(issue string, command string, labels []string) error {
log.Debug("label called")
if command != "add" && command != "remove" && command != "set" {
return fmt.Errorf("command must be 'add', 'set' or 'remove': %q is invalid", command)
}
handlePut := func(json string) error {
log.Debug("JSON: %s", json)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
if c.getOptBool("dryrun", false) {
log.Debug("PUT: %s", json)
log.Debug("Dryrun mode, skipping POST")
return nil
}
resp, err := c.put(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 204 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
return nil
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From PUT")
log.Error("%s:\n%s", err, logBuffer)
return err
}
}
var labels_json string
var err error
if command == "set" {
labelsCommands := make([]map[string][]string, 1)
labelsCommands[0] = map[string][]string{
"set": labels,
}
labels_json, err = jsonEncode(map[string]interface{}{
"labels": labelsCommands,
})
} else {
labelsCommands := make([]map[string]string, len(labels))
for i, label := range labels {
labelCommandMap := map[string]string{
command: label,
}
labelsCommands[i] = labelCommandMap
}
labels_json, err = jsonEncode(map[string]interface{}{
"labels": labelsCommands,
})
}
if err != nil {
return err
}
json := fmt.Sprintf("{ \"update\": %s }", labels_json)
return handlePut(json)
}
func (c *Cli) CmdAssign(issue string, user string) error {
log.Debug("assign called")
json, err := jsonEncode(map[string]interface{}{
"name": user,
})
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/assignee", c.endpoint, issue)
if c.getOptBool("dryrun", false) {
log.Debug("PUT: %s", json)
log.Debug("Dryrun mode, skipping PUT")
return nil
}
resp, err := c.put(uri, json)
if err != nil {
return err
}
if resp.StatusCode == 204 {
c.Browse(issue)
if !c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From PUT")
log.Error("%s:\n%s", err, logBuffer)
return err
}
return nil
}
func (c *Cli) CmdExportTemplates() error {
dir := c.opts["directory"].(string)
if err := mkdir(dir); err != nil {
return err
}
for name, template := range all_templates {
if wanted, ok := c.opts["template"]; ok && wanted != name {
continue
}
templateFile := fmt.Sprintf("%s/%s", dir, name)
if _, err := os.Stat(templateFile); err == nil {
log.Warning("Skipping %s, already exists", templateFile)
continue
}
if fh, err := os.OpenFile(templateFile, os.O_WRONLY|os.O_CREATE, 0644); err != nil {
log.Error("Failed to open %s for writing: %s", templateFile, err)
return err
} else {
defer fh.Close()
log.Notice("Creating %s", templateFile)
fh.Write([]byte(template))
}
}
return nil
}
func (c *Cli) CmdRequest(uri, content string) (err error) {
log.Debug("request called")
if !strings.HasPrefix(uri, "http") {
uri = fmt.Sprintf("%s%s", c.endpoint, uri)
}
method := strings.ToUpper(c.opts["method"].(string))
var data interface{}
if method == "GET" {
data, err = responseToJson(c.get(uri))
} else if method == "POST" {
data, err = responseToJson(c.post(uri, content))
} else if method == "PUT" {
data, err = responseToJson(c.put(uri, content))
}
if err != nil {
return err
}
return runTemplate(c.getTemplate("request"), data, nil)
}