From 18f10fd12521584c7d85b20dff2b1c2da0854cb9 Mon Sep 17 00:00:00 2001 From: Cory Bennett Date: Thu, 12 Feb 2015 23:41:39 -0800 Subject: [PATCH] adding commands: * create * dups * blocks * watch --- jira/cli/cli.go | 93 +++++++++++++++ jira/cli/commands.go | 271 +++++++++++++++++++++++++++--------------- jira/cli/templates.go | 29 ++++- jira/cli/util.go | 9 +- jira/main.go | 83 +++++++++++-- 5 files changed, 374 insertions(+), 111 deletions(-) diff --git a/jira/cli/cli.go b/jira/cli/cli.go index 4d11dcf..da7b4e1 100644 --- a/jira/cli/cli.go +++ b/jira/cli/cli.go @@ -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 +} diff --git a/jira/cli/commands.go b/jira/cli/commands.go index ccfdced..fcc12c8 100644 --- a/jira/cli/commands.go +++ b/jira/cli/commands.go @@ -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 +} + diff --git a/jira/cli/templates.go b/jira/cli/templates.go index 6360e2c..c96a83b 100644 --- a/jira/cli/templates.go +++ b/jira/cli/templates.go @@ -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: +` diff --git a/jira/cli/util.go b/jira/cli/util.go index a9d3393..7112344 100644 --- a/jira/cli/util.go +++ b/jira/cli/util.go @@ -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 } } + diff --git a/jira/main.go b/jira/main.go index cdaec2a..e454f93 100644 --- a/jira/main.go +++ b/jira/main.go @@ -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)) }