From acbc24b2096f31a5805fa48984724a4a6c1da431 Mon Sep 17 00:00:00 2001 From: Cory Bennett Date: Thu, 12 Feb 2015 15:50:08 -0800 Subject: [PATCH] work in progress, minor refactor. Added commands: * login * editmeta ISSUE * edit ISSUE * issuetypes [-p PROJECT] * createmeta [-p PROJECT] [-i ISSUETYPE] * transitions ISSUE make --template argumetn work --- jira/cli/cli.go | 107 ++++++++++++++---- jira/cli/commands.go | 251 +++++++++++++++++++++++++++++++++--------- jira/cli/templates.go | 34 +++++- jira/cli/util.go | 106 +++++++++++++++++- jira/main.go | 115 ++++++++++++++----- jira/util/util.go | 0 6 files changed, 504 insertions(+), 109 deletions(-) delete mode 100644 jira/util/util.go diff --git a/jira/cli/cli.go b/jira/cli/cli.go index d11bb7cd..4d11dcf9 100644 --- a/jira/cli/cli.go +++ b/jira/cli/cli.go @@ -10,7 +10,7 @@ import ( "os" "net/url" "time" - "io" + "bytes" "runtime" ) @@ -87,38 +87,99 @@ func (c *Cli) loadCookies() []*http.Cookie { return cookies } -func (c *Cli) post(uri string, content io.Reader) *http.Response { - req, _ := http.NewRequest("POST", uri, content) - return c.makeRequest(req) +func (c *Cli) post(uri string, content string) (*http.Response, error) { + return c.makeRequestWithContent("POST", uri, content) } -func (c *Cli) get(uri string) *http.Response { - req, _ := http.NewRequest("GET", uri, nil) - return c.makeRequest(req) +func (c *Cli) put(uri string, content string) (*http.Response, error) { + return c.makeRequestWithContent("PUT", uri, content) } -func (c *Cli) makeRequest(req *http.Request) *http.Response { - - req.Header.Set("Content-Type", "application/json") +func (c *Cli) makeRequestWithContent(method string, uri string, content string) (*http.Response, error) { + buffer := bytes.NewBufferString(content) + req, _ := http.NewRequest(method, uri, buffer) + + log.Info("%s %s", req.Method, req.URL.String()) + if log.IsEnabledFor(logging.DEBUG) { + logBuffer := bytes.NewBuffer(make([]byte,0,len(content))) + req.Write(logBuffer) + log.Debug("%s", logBuffer) + // need to recreate the buffer since the offset is now at the end + // need to be able to rewind the buffer offset, dont know how yet + req, _ = http.NewRequest(method, uri, bytes.NewBufferString(content)) + } - resp, err := c.ua.Do(req) - - if err != nil { - fmt.Printf("Error: %s", err) + if resp, err := c.makeRequest(req); err != nil { + return nil, err + } else { + if resp.StatusCode == 401 { + if err := c.CmdLogin(); err != nil { + return nil, err + } + req, _ = http.NewRequest(method, uri, bytes.NewBufferString(content)) + return c.makeRequest(req) + } + return resp, err } +} - if resp.StatusCode != 200 { - log.Error("response status: %s", resp.Status) - resp.Write(os.Stderr) +func (c *Cli) get(uri string) (*http.Response, error) { + req, _ := http.NewRequest("GET", uri, nil) + log.Info("%s %s", req.Method, req.URL.String()) + if log.IsEnabledFor(logging.DEBUG) { + logBuffer := bytes.NewBuffer(make([]byte,0)) + req.Write(logBuffer) + log.Debug("%s", logBuffer) } - runtime.SetFinalizer(resp, func(r *http.Response) { - r.Body.Close() - }) + if resp, err := c.makeRequest(req); err != nil { + return nil, err + } else { + if resp.StatusCode == 401 { + if err := c.CmdLogin(); err != nil { + return nil, err + } + return c.makeRequest(req) + } + return resp, err + } +} - if _, ok := resp.Header["Set-Cookie"]; ok { - c.saveCookies(resp.Cookies()) +func (c *Cli) makeRequest(req *http.Request) (resp *http.Response, err error) { + req.Header.Set("Content-Type", "application/json") + if resp, err = c.ua.Do(req); err != nil { + log.Error("Failed to %s %s: %s", req.Method, req.URL.String(), err) + return nil, err + } else { + if resp.StatusCode < 200 || resp.StatusCode >= 300 && resp.StatusCode != 401 { + log.Error("response status: %s", resp.Status) + resp.Write(os.Stderr) + } + + runtime.SetFinalizer(resp, func(r *http.Response) { + r.Body.Close() + }) + + if _, ok := resp.Header["Set-Cookie"]; ok { + c.saveCookies(resp.Cookies()) + } } + return resp, nil +} - return resp +func (c *Cli) getTemplate(path string, dflt string) string { + if override, ok := c.opts["template"]; ok { + if _, err := os.Stat(override); err == nil { + return readFile(override) + } else { + if file, err := FindClosestParentPath(fmt.Sprintf(".jira.d/templates/%s", override)); err == nil { + return readFile(file) + } + } + } + if file, err := FindClosestParentPath(path); err != nil { + return dflt + } else { + return readFile(file) + } } diff --git a/jira/cli/commands.go b/jira/cli/commands.go index 9087b6b3..ccfdcedf 100644 --- a/jira/cli/commands.go +++ b/jira/cli/commands.go @@ -2,17 +2,19 @@ package cli import ( "net/http" - "encoding/json" "fmt" - "bytes" - "os" "code.google.com/p/gopass" + "os" + "bytes" + "os/exec" + "io/ioutil" + "gopkg.in/yaml.v1" + // "github.com/kr/pretty" ) -func (c *Cli) CmdLogin() { +func (c *Cli) CmdLogin() (error) { uri := fmt.Sprintf("%s/rest/auth/1/session", c.endpoint) - resp := c.get(uri) - for ; resp.StatusCode != 200 ; { + for ; true ; { req, _ := http.NewRequest("GET", uri, nil) user, _ := c.opts["user"] @@ -20,75 +22,224 @@ func (c *Cli) CmdLogin() { passwd, _ := gopass.GetPass(prompt); req.SetBasicAuth(user, passwd) - resp = c.makeRequest(req) - 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 != "" { - log.Error("Authentication Failed: %s", reason) - os.Exit(1) + if resp, err := c.makeRequest(req); err != nil { + return err + } else { + 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 != "" { + log.Error("Authentication Failed: %s", reason) + return fmt.Errorf("Authenticaion Failed: %s", reason) + } + log.Error("Authentication Failead: Unknown") + return fmt.Errorf("Authentication Failead") + + } + if resp.StatusCode != 200 { + log.Warning("Login failed") + continue } - log.Error("Authentication Failead: Unknown") - os.Exit(1) - - } - if resp.StatusCode != 200 { - log.Error("Login failed") } + return nil } + return nil } -func (c *Cli) CmdFields() { +func (c *Cli) CmdFields() error { log.Debug("fields called") - resp := c.get(fmt.Sprintf("%s/rest/api/2/field", c.endpoint)) - data := jsonDecode(resp.Body) - - if templateFile, err := FindClosestParentPath(".jira.d/templates/fields"); err != nil { - runTemplate(default_fields_template, data) - } else { - log.Debug("Using Template: %s", templateFile) - runTemplate(readFile(templateFile), data) + 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(".jira.d/templates/fields", default_fields_template), data, nil) } -func (c *Cli) CmdList() { + +func (c *Cli) CmdList() error { log.Debug("list called") if query, ok := c.opts["query"]; !ok { log.Error("No query argument found, either use --query or set query attribute in .jira file") - os.Exit(1) + return fmt.Errorf("Missing query") } else { - buffer := bytes.NewBuffer(make([]byte, 0, len(query))) - enc := json.NewEncoder(buffer) - - enc.Encode(map[string]string{ + json, err := jsonEncode(map[string]string{ "jql": query, "startAt": "0", "maxResults": "500", - }) + }); if err != nil { + return err + } + + uri := fmt.Sprintf("%s/rest/api/2/search", c.endpoint) + data, err := responseToJson(c.post(uri, json)); if err != nil { + return err + } + + return runTemplate(c.getTemplate(".jira.d/templates/list", default_list_template), data, nil) + } +} + +func (c *Cli) CmdView(issue string) error { + log.Debug("view called") + 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(".jira.d/templates/view", default_view_template), data, nil) +} + +func (c *Cli) CmdEdit(issue string) error { + log.Debug("edit called") - resp := c.post(fmt.Sprintf("%s/rest/api/2/search", c.endpoint), buffer) - data := jsonDecode(resp.Body) + 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{})["fields"] + + 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 err + } + } - if templateFile, err := FindClosestParentPath(".jira.d/templates/list"); err != nil { - runTemplate(default_list_template, data) + if fixed, err := yamlFixup(edited); err != nil { + return err } else { - log.Debug("Using Template: %s", templateFile) - runTemplate(readFile(templateFile), data) + 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 + } + 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 { + log.Debug("editMeta called") + 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(".jira.d/templates/editmeta", default_fields_template), data, nil) } -func (c *Cli) CmdView(issue string) { - log.Debug("view called") - resp := c.get(fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)) - data := jsonDecode(resp.Body) - if templateFile, err := FindClosestParentPath(".jira.d/templates/view"); err != nil { - runTemplate(default_view_template, data) - } else { - log.Debug("Using Template: %s", templateFile) - runTemplate(readFile(templateFile), data) +func (c *Cli) CmdIssueTypes(project string) error { + 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(".jira.d/templates/issuetypes", default_issuetypes_template), data, nil) } +func (c *Cli) CmdCreateMeta(project string, issuetype string) error { + 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 + } + + return runTemplate(c.getTemplate(".jira.d/templates/createmeta", default_fields_template), data, nil) +} + +func (c *Cli) CmdTransitions(issue string) error { + log.Debug("Transitions called") + 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(".jira.d/templates/transitions", default_transitions_template), data, nil) +} diff --git a/jira/cli/templates.go b/jira/cli/templates.go index f5b677c8..6360e2c2 100644 --- a/jira/cli/templates.go +++ b/jira/cli/templates.go @@ -5,6 +5,7 @@ const default_fields_template = "{{ . | toJson}}\n" const default_list_template = "{{ range .issues }}{{ .key | append \":\" | printf \"%-12s\"}} {{ .fields.summary }}\n{{ end }}" const default_view_template = `issue: {{ .key }} +status: {{ .fields.status.name }} summary: {{ .fields.summary }} project: {{ .fields.project.key }} components: {{ range .fields.components }}{{ .name }} {{end}} @@ -12,13 +13,40 @@ issuetype: {{ .fields.issuetype.name }} assignee: {{ .fields.assignee.name }} reporter: {{ .fields.reporter.name }} watchers: {{ range .fields.customfield_10110 }}{{ .name }} {{end}} -blockers: {{ range .fields.issuelinks }}{{if .outwardIssue}}{{ .outwardIssue.key }}{{end}}{{end}} -depends: {{ range .fields.issuelinks }}{{if .inwardIssue}}{{ .inwardIssue.key }}{{end}}{{end}} +blockers: {{ range .fields.issuelinks }}{{if .outwardIssue}}{{ .outwardIssue.key }}[{{.outwardIssue.fields.status.name}}]{{end}}{{end}} +depends: {{ range .fields.issuelinks }}{{if .inwardIssue}}{{ .inwardIssue.key }}[{{.inwardIssue.fields.status.name}}]{{end}}{{end}} priority: {{ .fields.priority.name }} description: | {{ .fields.description | indent 2 }} comments: {{ range .fields.comment.comments }} - | # {{.author.name}} at {{.created}} - {{ .body | indent 4}}{{end}} + {{ .body | indent 4}} +{{end}} ` +const default_edit_template = `update: + comment: + - add: + body: | + +fields: + summary: {{ .fields.summary }} + components: # {{ range .meta.components.allowedValues }}{{.name}}, {{end}}{{ range .fields.components }} + - name: {{ .name }}{{end}} + assignee: + name: {{ .fields.assignee.name }} + reporter: + name: {{ .fields.reporter.name }} + # watchers + customfield_10110: {{ range .fields.customfield_10110 }} + - name: {{ .name }}{{end}} + priority: # {{ range .meta.priority.allowedValues }}{{.name}}, {{end}} + name: {{ .fields.priority.name }} + description: | + {{ .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}}` diff --git a/jira/cli/util.go b/jira/cli/util.go index 5dae2657..a9d33939 100644 --- a/jira/cli/util.go +++ b/jira/cli/util.go @@ -5,11 +5,13 @@ import ( "fmt" "errors" "strings" - + "net/http" "encoding/json" "io/ioutil" "text/template" "io" + "bufio" + "bytes" "github.com/mgutz/ansi" ) @@ -18,6 +20,15 @@ func FindParentPaths(fileName string) []string { paths := make([]string,0) + // special case if homedir is not in current path then check there anyway + homedir := os.Getenv("HOME") + if ! strings.HasPrefix(cwd, homedir) { + file := fmt.Sprintf("%s/%s", homedir, fileName) + if _, err := os.Stat(file); err == nil { + paths = append(paths, file) + } + } + var dir string for _, part := range strings.Split(cwd, string(os.PathSeparator)) { if dir == "/" { @@ -51,7 +62,12 @@ func readFile(file string) string { return string(bytes) } -func runTemplate(text string, data interface{}) { +func runTemplate(templateContent string, data interface{}, out io.Writer) error { + + if out == nil { + out = os.Stdout + } + funcs := map[string]interface{}{ "toJson": func(content interface{}) (string, error) { if bytes, err := json.MarshalIndent(content, "", " "); err != nil { @@ -79,15 +95,24 @@ func runTemplate(text string, data interface{}) { return ansi.ColorCode(color) }, } - if tmpl, err := template.New("template").Funcs(funcs).Parse(text); err != nil { + if tmpl, err := template.New("template").Funcs(funcs).Parse(templateContent); err != nil { log.Error("Failed to parse template: %s", err) - os.Exit(1) + return err } else { - if err := tmpl.Execute(os.Stdout, data); err != nil { + if err := tmpl.Execute(out, data); err != nil { log.Error("Failed to execute template: %s", err) - os.Exit(1) + return err } } + return nil +} + +func responseToJson(resp *http.Response, err error) (interface{}, error) { + if err != nil { + return nil, err + } else { + return jsonDecode(resp.Body), nil + } } func jsonDecode(io io.Reader) interface{} { @@ -100,6 +125,17 @@ func jsonDecode(io io.Reader) interface{} { return data } +func jsonEncode(data interface{}) (string, error) { + buffer := bytes.NewBuffer(make([]byte, 0)) + enc := json.NewEncoder(buffer) + + err := enc.Encode(data); if err != nil { + log.Error("Failed to encode data %s: %s", data, err) + return "", err + } + return buffer.String(), nil +} + func jsonWrite(file string, data interface{}) { fh, err := os.OpenFile(file, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0600) defer fh.Close() @@ -111,3 +147,61 @@ func jsonWrite(file string, data interface{}) { enc.Encode(data) } +func promptYN(prompt string, yes bool) bool { + reader := bufio.NewReader(os.Stdin) + if !yes { + prompt = fmt.Sprintf("%s [y/N]: ", prompt) + } else { + prompt = fmt.Sprintf("%s [Y/n]: ", prompt) + } + + fmt.Printf("%s", prompt) + text, _ := reader.ReadString('\n') + ans := strings.ToLower(text) + if( strings.HasPrefix(ans, "y") ) { + return true + } + return false +} + +func yamlFixup( data interface{} ) (interface{}, error) { + switch d := data.(type) { + case map[interface{}]interface{}: + // need to copy this map into a string map so json can encode it + copy := make(map[string]interface{}) + for key, val := range d { + switch k := key.(type) { + case string: + if fixed, err := yamlFixup(val); err != nil { + return nil, err + } else { + copy[k] = fixed + } + default: + err := fmt.Errorf("YAML: key %s is type '%T', require 'string'", key, k) + log.Error("%s", err) + return nil, err + } + } + return copy, nil + case map[string]interface{}: + for k, v := range d { + if fixed, err := yamlFixup(v); err != nil { + return nil, err + } else { + d[k] = fixed + } + } + return d, nil + case []interface{}: + for i, val := range d { + if fixed, err := yamlFixup(val); err != nil { + return nil, err + } else { + d[i] = fixed + } + } + return data, nil + default: return d, nil + } +} diff --git a/jira/main.go b/jira/main.go index eb61d02e..cdaec2a4 100644 --- a/jira/main.go +++ b/jira/main.go @@ -14,32 +14,29 @@ import ( var log = logging.MustGetLogger("jira") var format = "%{color}%{time:2006-01-02T15:04:05.000Z07:00} %{level:-5s} [%{shortfile}]%{color:reset} %{message}" -func parseYaml(file string, opts map[string]string) { - if fh, err := ioutil.ReadFile(file); err == nil { - log.Debug("Found Config file: %s", file) - yaml.Unmarshal(fh, &opts) - } -} - -func loadConfigs(opts map[string]string) { - paths := cli.FindParentPaths(".jira.d/config.yml") - // prepend - paths = append([]string{"/etc/jira-cli.yml"}, paths...) - - for _, file := range(paths) { - parseYaml(file, opts) - } -} - func main() { user := os.Getenv("USER") usage := fmt.Sprintf(` Usage: jira [-v ...] [-u USER] [-e URI] [-t FILE] fields - jira [-v ...] [-u USER] [-e URI] [-t FILE] ls [--query=JQL] + 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] 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] 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 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] + jira TODO [-v ...] [-u USER] [-e URI] take ISSUE + jira TODO [-v ...] [-u USER] [-e URI] assign ISSUE ASSIGNEE General Options: -h --help Show this usage @@ -49,8 +46,12 @@ General Options: -e --endpoint=URI URI to use for jira (default: https://jira) -t --template=FILE Template file to use for output -List options: +List Options: -q --query=JQL Jira Query Language expression for the search + +Create Options: + -p --project=PROJECT Jira Project Name + -i --issuetype=ISSUETYPE Jira Issue Type (default: Bug) `, user) args, _ := docopt.Parse(usage, nil, true, "0.0.1", false, false) @@ -76,36 +77,96 @@ List options: opts := make(map[string]string) loadConfigs(opts) + // strip the "--" off the command line options + // and populate the opts that we pass to the cli ctor 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 } } } + // cant use proper [default:x] syntax in docopt + // because only want to default if the option is not + // already specified in some .jira.d/config.yml file if _, ok := opts["endpoint"]; !ok { opts["endpoint"] = "https://jira" } if _, ok := opts["user"]; !ok { opts["user"] = user } + if _, ok := opts["issuetype"]; !ok { + opts["issuetype"] = "Bug" + } c := cli.New(opts) log.Debug("opts: %s", opts); - - c.CmdLogin() - - if val, ok := args["fields"]; ok && val.(bool) { - c.CmdFields() + + var err error + if val, ok := args["login"]; ok && val.(bool) { + err = c.CmdLogin() + } else if val, ok := args["fields"]; ok && val.(bool) { + err = c.CmdFields() } else if val, ok := args["ls"]; ok && val.(bool) { - c.CmdList() + err = c.CmdList() + } else if val, ok := args["edit"]; ok && val.(bool) { + issue, _ := args["ISSUE"] + err = c.CmdEdit(issue.(string)) + } else if val, ok := args["editmeta"]; ok && val.(bool) { + issue, _ := args["ISSUE"] + err = c.CmdEditMeta(issue.(string)) + } else if val, ok := args["issuetypes"]; 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) + } + err = c.CmdIssueTypes(project.(string)) + } else if val, ok := args["createmeta"]; 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.CmdCreateMeta(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["ISSUE"]; ok { - c.CmdView(val.(string)) + err = c.CmdView(val.(string)) } + if err != nil { + os.Exit(1) + } os.Exit(0) } + +func parseYaml(file string, opts map[string]string) { + if fh, err := ioutil.ReadFile(file); err == nil { + log.Debug("Found Config file: %s", file) + yaml.Unmarshal(fh, &opts) + } +} + +func loadConfigs(opts map[string]string) { + paths := cli.FindParentPaths(".jira.d/config.yml") + // prepend + paths = append([]string{"/etc/jira-cli.yml"}, paths...) + + for _, file := range(paths) { + parseYaml(file, opts) + } +} + diff --git a/jira/util/util.go b/jira/util/util.go deleted file mode 100644 index e69de29b..00000000