mirror of
https://github.com/Threnklyn/jira.git
synced 2026-05-18 20:23:28 +02:00
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
This commit is contained in:
+89
-28
@@ -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 {
|
||||
func (c *Cli) put(uri string, content string) (*http.Response, error) {
|
||||
return c.makeRequestWithContent("PUT", uri, content)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) get(uri string) (*http.Response, error) {
|
||||
req, _ := http.NewRequest("GET", uri, nil)
|
||||
return c.makeRequest(req)
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) makeRequest(req *http.Request) *http.Response {
|
||||
|
||||
func (c *Cli) makeRequest(req *http.Request) (resp *http.Response, err error) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.ua.Do(req)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %s", err)
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
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
|
||||
}
|
||||
|
||||
+204
-53
@@ -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
|
||||
}
|
||||
|
||||
resp := c.post(fmt.Sprintf("%s/rest/api/2/search", c.endpoint), buffer)
|
||||
data := jsonDecode(resp.Body)
|
||||
|
||||
if templateFile, err := FindClosestParentPath(".jira.d/templates/list"); err != nil {
|
||||
runTemplate(default_list_template, data)
|
||||
} else {
|
||||
log.Debug("Using Template: %s", templateFile)
|
||||
runTemplate(readFile(templateFile), data)
|
||||
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")
|
||||
|
||||
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 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
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
json, err := jsonEncode(edited); if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.put(uri, json); if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
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) 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)
|
||||
}
|
||||
|
||||
+31
-3
@@ -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}}`
|
||||
|
||||
+100
-6
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+87
-26
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user