adding commands:

* create
* dups
* blocks
* watch
This commit is contained in:
Cory Bennett
2015-02-12 23:41:39 -08:00
parent acbc24b209
commit 18f10fd125
5 changed files with 374 additions and 111 deletions
+93
View File
@@ -8,6 +8,8 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"gopkg.in/yaml.v2"
"net/url"
"time"
"bytes"
@@ -183,3 +185,94 @@ func (c *Cli) getTemplate(path string, dflt string) string {
return readFile(file)
}
}
func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData map[string]interface{}, templateProcessor func(string) error) error {
tmpdir := fmt.Sprintf("%s/.jira.d/tmp", os.Getenv("HOME"))
fh, err := ioutil.TempFile(tmpdir, tmpFilePrefix); if err != nil {
log.Error("Failed to make temp file in %s: %s", tmpdir, err)
return err
}
defer fh.Close()
tmpFileName := fmt.Sprintf("%s.yml", fh.Name())
if err := os.Rename(fh.Name(), tmpFileName); err != nil {
log.Error("Failed to rename %s to %s: %s", fh.Name(), fmt.Sprintf("%s.yml", fh.Name()), err)
return err
}
err = runTemplate(template, templateData, fh); if err != nil {
return err
}
fh.Close()
editor, ok := c.opts["editor"]; if !ok {
editor = os.Getenv("JIRA_EDITOR"); if editor == "" {
editor = os.Getenv("EDITOR"); if editor == "" {
editor = "vim"
}
}
}
for ; true ; {
log.Debug("Running: %s %s", editor, tmpFileName)
cmd := exec.Command(editor, tmpFileName)
cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin
if err := cmd.Run(); err != nil {
log.Error("Failed to edit template with %s: %s", editor, err)
if promptYN("edit again?", true) {
continue
}
return err
}
edited := make(map[string]interface{})
if fh, err := ioutil.ReadFile(tmpFileName); err != nil {
log.Error("Failed to read tmpfile %s: %s", tmpFileName, err)
if promptYN("edit again?", true) {
continue
}
return err
} else {
if err := yaml.Unmarshal(fh, &edited); err != nil {
log.Error("Failed to parse YAML: %s", err)
if promptYN("edit again?", true) {
continue
}
return err
}
}
if fixed, err := yamlFixup(edited); err != nil {
return err
} else {
edited = fixed.(map[string]interface{})
}
mf := templateData["meta"].(map[string]interface{})["fields"]
f := edited["fields"].(map[string]interface{})
for k, _ := range f {
if _, ok := mf.(map[string]interface{})[k]; !ok {
err := fmt.Errorf("Field %s is not editable", k)
log.Error("%s", err)
if promptYN("edit again?", true) {
continue
}
return err
}
}
json, err := jsonEncode(edited); if err != nil {
return err
}
if err := templateProcessor(json); err != nil {
log.Error("%s", err)
if promptYN("edit again?", true) {
continue
}
}
return nil
}
return nil
}
+176 -95
View File
@@ -4,11 +4,8 @@ import (
"net/http"
"fmt"
"code.google.com/p/gopass"
"os"
"bytes"
"os/exec"
"io/ioutil"
"gopkg.in/yaml.v1"
"strings"
// "github.com/kr/pretty"
)
@@ -107,102 +104,30 @@ func (c *Cli) CmdEdit(issue string) error {
issueData = data.(map[string]interface{})
}
issueData["meta"] = editmeta.(map[string]interface{})["fields"]
issueData["meta"] = editmeta.(map[string]interface{})
issueData["overrides"] = c.opts
tmpdir := fmt.Sprintf("%s/.jira.d/tmp", os.Getenv("HOME"))
fh, err := ioutil.TempFile(tmpdir, fmt.Sprintf("%s-edit-", issue)); if err != nil {
log.Error("Failed to make temp file in %s: %s", tmpdir, err)
return err
}
defer fh.Close()
tmpFileName := fmt.Sprintf("%s.yml", fh.Name())
if err := os.Rename(fh.Name(), tmpFileName); err != nil {
log.Error("Failed to rename %s to %s: %s", fh.Name(), fmt.Sprintf("%s.yml", fh.Name()), err)
return err
}
err = runTemplate(c.getTemplate(".jira.d/templates/edit", default_edit_template), issueData, fh); if err != nil {
return err
}
fh.Close()
editor, ok := c.opts["editor"]; if !ok {
editor = os.Getenv("JIRA_EDITOR"); if editor == "" {
editor = os.Getenv("EDITOR"); if editor == "" {
editor = "vim"
}
}
}
for ; true ; {
log.Debug("Running: %s %s", editor, tmpFileName)
cmd := exec.Command(editor, tmpFileName)
cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin
if err := cmd.Run(); err != nil {
log.Error("Failed to edit template with %s: %s", editor, err)
if promptYN("edit again?", true) {
continue
}
return err
}
edited := make(map[string]interface{})
if fh, err := ioutil.ReadFile(tmpFileName); err != nil {
log.Error("Failed to read tmpfile %s: %s", tmpFileName, err)
if promptYN("edit again?", true) {
continue
}
return err
} else {
if err := yaml.Unmarshal(fh, &edited); err != nil {
log.Error("Failed to parse YAML: %s", err)
if promptYN("edit again?", true) {
continue
}
return c.editTemplate(
c.getTemplate(".jira.d/templates/edit", default_edit_template),
fmt.Sprintf("%s-edit-", issue),
issueData,
func(json string) error {
resp, err := c.put(uri, json); if err != nil {
return err
}
}
if fixed, err := yamlFixup(edited); err != nil {
return err
} else {
edited = fixed.(map[string]interface{})
}
mf := editmeta.(map[string]interface{})["fields"]
f := edited["fields"].(map[string]interface{})
for k, _ := range f {
if _, ok := mf.(map[string]interface{})[k]; !ok {
err := fmt.Errorf("Field %s is not editable", k)
log.Error("%s", err)
if promptYN("edit again?", true) {
continue
}
if resp.StatusCode == 204 {
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
}
}
json, err := jsonEncode(edited); if err != nil {
return err
}
resp, err := c.put(uri, json); if err != nil {
return err
}
if resp.StatusCode == 204 {
fmt.Printf("OK %s %s", issueData["key"], issueData["self"])
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
}
}
return nil
},
)
}
func (c *Cli) CmdEditMeta(issue string) error {
@@ -232,6 +157,12 @@ func (c *Cli) CmdCreateMeta(project string, issuetype string) error {
return err
}
if val, ok := data.(map[string]interface{})["projects"]; ok {
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
data = val.([]interface{})[0]
}
}
return runTemplate(c.getTemplate(".jira.d/templates/createmeta", default_fields_template), data, nil)
}
@@ -243,3 +174,153 @@ func (c *Cli) CmdTransitions(issue string) error {
}
return runTemplate(c.getTemplate(".jira.d/templates/transitions", default_transitions_template), data, nil)
}
func (c *Cli) CmdCreate(project string, issuetype string) error {
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
if val, ok := data.(map[string]interface{})["projects"]; ok {
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(".jira.d/templates/create-%s", sanitizedType), default_create_template),
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)
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"]
fmt.Printf("OK %s %s/browse/%s\n", key, c.endpoint, 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
}
},
)
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(".jira.d/templates/issuelinktypes", default_fields_template), 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",
},
"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)
resp, err := c.post(uri, json); if err != nil {
return err
}
if resp.StatusCode == 201 {
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) CmdDups(duplicate string, issue string) error {
log.Debug("dups called")
json, err := jsonEncode(map[string]interface{}{
"type": map[string]string{
"name": "Duplicate",
},
"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)
resp, err := c.post(uri, json); if err != nil {
return err
}
if resp.StatusCode == 201 {
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) CmdWatch(issue string, watcher string) error {
log.Debug("dups called")
json, err := jsonEncode(watcher); if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", c.endpoint, issue)
resp, err := c.post(uri, json); if err != nil {
return err
}
if resp.StatusCode == 204 {
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
}
+25 -4
View File
@@ -31,22 +31,43 @@ const default_edit_template = `update:
fields:
summary: {{ .fields.summary }}
components: # {{ range .meta.components.allowedValues }}{{.name}}, {{end}}{{ range .fields.components }}
components: # {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range .fields.components }}
- name: {{ .name }}{{end}}
assignee:
name: {{ .fields.assignee.name }}
name: {{ if .fields.assignee }}{{ .fields.assignee.name }}{{end}}
reporter:
name: {{ .fields.reporter.name }}
# watchers
customfield_10110: {{ range .fields.customfield_10110 }}
- name: {{ .name }}{{end}}
priority: # {{ range .meta.priority.allowedValues }}{{.name}}, {{end}}
priority: # {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ .fields.priority.name }}
description: |
{{ .fields.description | indent 4 }}
{{ or .fields.description "" | indent 4 }}
`
const default_transitions_template = `{{ range .transitions }}{{color "+bh"}}{{.name | printf "%-13s" }}{{color "reset"}} -> {{.to.name}}
{{end}}`
const default_issuetypes_template = `{{ range .projects }}{{ range .issuetypes }}{{color "+bh"}}{{.name | append ":" | printf "%-13s" }}{{color "reset"}} {{.description}}
{{end}}{{end}}`
const default_create_template = `fields:
project:
key: {{ .overrides.project }}
issuetype:
name: {{ .overrides.issuetype }}
summary: {{ or .overrides.summary "" }}
priority: # {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ or .overrides.priority "" }}
components: # {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}}
- name: {{ . }}{{end}}
description: |
{{ or .overrides.description "" | indent 4 }}
assignee:
name: {{ or .overrides.assignee .overrides.user}}
reporter:
name: {{ or .overrides.reporter .overrides.user }}
# watchers
customfield_10110:
- name:
`
+8 -1
View File
@@ -94,6 +94,9 @@ func runTemplate(templateContent string, data interface{}, out io.Writer) error
"color": func(color string) string {
return ansi.ColorCode(color)
},
"split": func(sep string, content string) []string {
return strings.Split(content, sep)
},
}
if tmpl, err := template.New("template").Funcs(funcs).Parse(templateContent); err != nil {
log.Error("Failed to parse template: %s", err)
@@ -157,7 +160,10 @@ func promptYN(prompt string, yes bool) bool {
fmt.Printf("%s", prompt)
text, _ := reader.ReadString('\n')
ans := strings.ToLower(text)
ans := strings.ToLower(strings.TrimRight(text, "\n"))
if ans == "" {
return yes
}
if( strings.HasPrefix(ans, "y") ) {
return true
}
@@ -205,3 +211,4 @@ func yamlFixup( data interface{} ) (interface{}, error) {
default: return d, nil
}
}
+72 -11
View File
@@ -22,16 +22,18 @@ Usage:
jira [-v ...] [-u USER] [-e URI] [-t FILE] login
jira [-v ...] [-u USER] [-e URI] [-t FILE] ls [-q JQL]
jira [-v ...] [-u USER] [-e URI] [-t FILE] view ISSUE
jira [-v ...] [-u USER] [-e URI] [-t FILE] issuelinktypes
jira [-v ...] [-u USER] [-e URI] [-t FILE] ISSUE
jira [-v ...] [-u USER] [-e URI] [-t FILE] editmeta ISSUE
jira [-v ...] [-u USER] [-e URI] [-t FILE] edit ISSUE
jira [-v ...] [-u USER] [-e URI] [-t FILE] edit ISSUE [-o KEY=VAL]...
jira [-v ...] [-u USER] [-e URI] [-t FILE] issuetypes [-p PROJECT]
jira [-v ...] [-u USER] [-e URI] [-t FILE] createmeta [-p PROJECT] [-i ISSUETYPE]
jira [-v ...] [-u USER] [-e URI] [-t FILE] transitions ISSUE
jira [-v ...] [-u USER] [-e URI] [-t FILE] create [-p PROJECT] [-i ISSUETYPE] [-o KEY=VAL]...
jira [-v ...] [-u USER] [-e URI] DUPLICATE dups ISSUE
jira [-v ...] [-u USER] [-e URI] BLOCKER blocks ISSUE
jira [-v ...] [-u USER] [-e URI] watch ISSUE [WATCHER]
jira TODO [-v ...] [-u USER] [-e URI] [-t FILE] create [-p PROJECT] [-i ISSUETYPE]
jira TODO [-v ...] [-u USER] [-e URI] DUPLICATE dups ISSUE
jira TODO [-v ...] [-u USER] [-e URI] BLOCKER blocks ISSUE
jira TODO [-v ...] [-u USER] [-e URI] close ISSUE [-m COMMENT]
jira TODO [-v ...] [-u USER] [-e URI] resolve ISSUE [-m COMMENT]
jira TODO [-v ...] [-u USER] [-e URI] comment ISSUE [-m COMMENT]
@@ -52,6 +54,7 @@ List Options:
Create Options:
-p --project=PROJECT Jira Project Name
-i --issuetype=ISSUETYPE Jira Issue Type (default: Bug)
-o --override=KEY:VAL Set custom key/value pairs
`, user)
args, _ := docopt.Parse(usage, nil, true, "0.0.1", false, false)
@@ -73,7 +76,6 @@ Create Options:
log.Info("Args: %v", args)
opts := make(map[string]string)
loadConfigs(opts)
@@ -82,12 +84,23 @@ Create Options:
for key,val := range args {
if val != nil && strings.HasPrefix(key, "--") {
opt := key[2:]
switch v := val.(type) {
// only deal with string opts, ignore
// other types, like int (for now) since
// they are only used for --verbose
case string:
opts[opt] = v
if opt == "override" {
for _, v := range val.([]string) {
if strings.Contains(v, "=") {
kv := strings.SplitN(v, "=", 2)
opts[kv[0]] = kv[1]
} else {
log.Error("Malformed override, expected KEY=VALUE, got %s", v)
os.Exit(1)
}
}
} else {
switch v := val.(type) {
case string:
opts[opt] = v
case int:
opts[opt] = fmt.Sprintf("%d", v)
}
}
}
}
@@ -122,6 +135,8 @@ Create Options:
} else if val, ok := args["editmeta"]; ok && val.(bool) {
issue, _ := args["ISSUE"]
err = c.CmdEditMeta(issue.(string))
} else if val, ok := args["issuelinktypes"]; ok && val.(bool) {
err = c.CmdIssueLinkTypes()
} else if val, ok := args["issuetypes"]; ok && val.(bool) {
var project interface{}
if project, ok = opts["project"]; !ok {
@@ -140,9 +155,55 @@ Create Options:
issuetype = "Bug"
}
err = c.CmdCreateMeta(project.(string), issuetype.(string))
} else if val, ok := args["create"]; ok && val.(bool) {
var project interface{}
if project, ok = opts["project"]; !ok {
log.Error("missing PROJECT argument or \"project\" property in the config file")
os.Exit(1)
}
var issuetype interface{}
if issuetype, ok = opts["issuetype"]; !ok {
issuetype = "Bug"
}
err = c.CmdCreate(project.(string), issuetype.(string))
} else if val, ok := args["transitions"]; ok && val.(bool) {
issue, _ := args["ISSUE"]
err = c.CmdTransitions(issue.(string))
} else if val, ok := args["blocks"]; ok && val.(bool) {
if blocker, ok := args["BLOCKER"].(string); ok {
if issue, ok := args["ISSUE"].(string); ok {
err = c.CmdBlocks(blocker, issue)
} else {
log.Error("missing ISSUE")
os.Exit(1)
}
} else {
log.Error("missing BLOCKER")
os.Exit(1)
}
} else if val, ok := args["dups"]; ok && val.(bool) {
if duplicate, ok := args["DUPLICATE"].(string); ok {
if issue, ok := args["ISSUE"].(string); ok {
err = c.CmdDups(duplicate, issue)
} else {
log.Error("missing ISSUE")
os.Exit(1)
}
} else {
log.Error("missing BLOCKER")
os.Exit(1)
}
} else if val, ok := args["watch"]; ok && val.(bool) {
if issue, ok := args["ISSUE"].(string); ok {
var watcher string
if watcher, ok = args["WATCHER"].(string); !ok {
watcher = user
}
err = c.CmdWatch(issue, watcher)
} else {
log.Error("missing ISSUE")
os.Exit(1)
}
} else if val, ok := args["ISSUE"]; ok {
err = c.CmdView(val.(string))
}