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" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec"
"gopkg.in/yaml.v2"
"net/url" "net/url"
"time" "time"
"bytes" "bytes"
@@ -183,3 +185,94 @@ func (c *Cli) getTemplate(path string, dflt string) string {
return readFile(file) 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" "net/http"
"fmt" "fmt"
"code.google.com/p/gopass" "code.google.com/p/gopass"
"os"
"bytes" "bytes"
"os/exec" "strings"
"io/ioutil"
"gopkg.in/yaml.v1"
// "github.com/kr/pretty" // "github.com/kr/pretty"
) )
@@ -107,102 +104,30 @@ func (c *Cli) CmdEdit(issue string) error {
issueData = data.(map[string]interface{}) 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")) return c.editTemplate(
fh, err := ioutil.TempFile(tmpdir, fmt.Sprintf("%s-edit-", issue)); if err != nil { c.getTemplate(".jira.d/templates/edit", default_edit_template),
log.Error("Failed to make temp file in %s: %s", tmpdir, err) fmt.Sprintf("%s-edit-", issue),
return err issueData,
} func(json string) error {
defer fh.Close() resp, err := c.put(uri, json); if err != nil {
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 err return err
} }
}
if resp.StatusCode == 204 {
if fixed, err := yamlFixup(edited); err != nil { fmt.Printf("OK %s %s/browse/%s\n", issueData["key"], c.endpoint, issueData["key"])
return err return nil
} else { } else {
edited = fixed.(map[string]interface{}) logBuffer := bytes.NewBuffer(make([]byte,0))
} resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From PUT")
mf := editmeta.(map[string]interface{})["fields"] log.Error("%s:\n%s", err, logBuffer)
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 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 { func (c *Cli) CmdEditMeta(issue string) error {
@@ -232,6 +157,12 @@ func (c *Cli) CmdCreateMeta(project string, issuetype string) error {
return err 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) 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) 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: fields:
summary: {{ .fields.summary }} 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}} - name: {{ .name }}{{end}}
assignee: assignee:
name: {{ .fields.assignee.name }} name: {{ if .fields.assignee }}{{ .fields.assignee.name }}{{end}}
reporter: reporter:
name: {{ .fields.reporter.name }} name: {{ .fields.reporter.name }}
# watchers # watchers
customfield_10110: {{ range .fields.customfield_10110 }} customfield_10110: {{ range .fields.customfield_10110 }}
- name: {{ .name }}{{end}} - name: {{ .name }}{{end}}
priority: # {{ range .meta.priority.allowedValues }}{{.name}}, {{end}} priority: # {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ .fields.priority.name }} name: {{ .fields.priority.name }}
description: | 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}} const default_transitions_template = `{{ range .transitions }}{{color "+bh"}}{{.name | printf "%-13s" }}{{color "reset"}} -> {{.to.name}}
{{end}}` {{end}}`
const default_issuetypes_template = `{{ range .projects }}{{ range .issuetypes }}{{color "+bh"}}{{.name | append ":" | printf "%-13s" }}{{color "reset"}} {{.description}} const default_issuetypes_template = `{{ range .projects }}{{ range .issuetypes }}{{color "+bh"}}{{.name | append ":" | printf "%-13s" }}{{color "reset"}} {{.description}}
{{end}}{{end}}` {{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 { "color": func(color string) string {
return ansi.ColorCode(color) 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 { if tmpl, err := template.New("template").Funcs(funcs).Parse(templateContent); err != nil {
log.Error("Failed to parse template: %s", err) log.Error("Failed to parse template: %s", err)
@@ -157,7 +160,10 @@ func promptYN(prompt string, yes bool) bool {
fmt.Printf("%s", prompt) fmt.Printf("%s", prompt)
text, _ := reader.ReadString('\n') text, _ := reader.ReadString('\n')
ans := strings.ToLower(text) ans := strings.ToLower(strings.TrimRight(text, "\n"))
if ans == "" {
return yes
}
if( strings.HasPrefix(ans, "y") ) { if( strings.HasPrefix(ans, "y") ) {
return true return true
} }
@@ -205,3 +211,4 @@ func yamlFixup( data interface{} ) (interface{}, error) {
default: return d, nil 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] login
jira [-v ...] [-u USER] [-e URI] [-t FILE] ls [-q JQL] 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] 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] ISSUE
jira [-v ...] [-u USER] [-e URI] [-t FILE] editmeta 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] issuetypes [-p PROJECT]
jira [-v ...] [-u USER] [-e URI] [-t FILE] createmeta [-p PROJECT] [-i ISSUETYPE] 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] 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] close ISSUE [-m COMMENT]
jira TODO [-v ...] [-u USER] [-e URI] resolve 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] jira TODO [-v ...] [-u USER] [-e URI] comment ISSUE [-m COMMENT]
@@ -52,6 +54,7 @@ List Options:
Create Options: Create Options:
-p --project=PROJECT Jira Project Name -p --project=PROJECT Jira Project Name
-i --issuetype=ISSUETYPE Jira Issue Type (default: Bug) -i --issuetype=ISSUETYPE Jira Issue Type (default: Bug)
-o --override=KEY:VAL Set custom key/value pairs
`, user) `, user)
args, _ := docopt.Parse(usage, nil, true, "0.0.1", false, false) args, _ := docopt.Parse(usage, nil, true, "0.0.1", false, false)
@@ -73,7 +76,6 @@ Create Options:
log.Info("Args: %v", args) log.Info("Args: %v", args)
opts := make(map[string]string) opts := make(map[string]string)
loadConfigs(opts) loadConfigs(opts)
@@ -82,12 +84,23 @@ Create Options:
for key,val := range args { for key,val := range args {
if val != nil && strings.HasPrefix(key, "--") { if val != nil && strings.HasPrefix(key, "--") {
opt := key[2:] opt := key[2:]
switch v := val.(type) { if opt == "override" {
// only deal with string opts, ignore for _, v := range val.([]string) {
// other types, like int (for now) since if strings.Contains(v, "=") {
// they are only used for --verbose kv := strings.SplitN(v, "=", 2)
case string: opts[kv[0]] = kv[1]
opts[opt] = v } 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) { } else if val, ok := args["editmeta"]; ok && val.(bool) {
issue, _ := args["ISSUE"] issue, _ := args["ISSUE"]
err = c.CmdEditMeta(issue.(string)) 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) { } else if val, ok := args["issuetypes"]; ok && val.(bool) {
var project interface{} var project interface{}
if project, ok = opts["project"]; !ok { if project, ok = opts["project"]; !ok {
@@ -140,9 +155,55 @@ Create Options:
issuetype = "Bug" issuetype = "Bug"
} }
err = c.CmdCreateMeta(project.(string), issuetype.(string)) 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) { } else if val, ok := args["transitions"]; ok && val.(bool) {
issue, _ := args["ISSUE"] issue, _ := args["ISSUE"]
err = c.CmdTransitions(issue.(string)) 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 { } else if val, ok := args["ISSUE"]; ok {
err = c.CmdView(val.(string)) err = c.CmdView(val.(string))
} }