Compare commits

...

11 Commits

Author SHA1 Message Date
Cory Bennett f95aa3d261 bump version 2015-02-19 13:09:32 -08:00
Cory Bennett 4d95bde10f add --template option to export-templates to export a single template 2015-02-19 13:07:46 -08:00
Cory Bennett b35b8d1fd1 update for table template 2015-02-19 12:38:12 -08:00
Cory Bennett fd4ec5e641 add "table" template to be used with "list" command 2015-02-19 12:36:51 -08:00
Cory Bennett 0b88d0ad97 prevent line wrapping 2015-02-19 11:17:33 -08:00
Cory Bennett 8c07442645 bump version 2015-02-19 10:31:20 -08:00
Cory Bennett f3feff796f [issue #8] detect X-Seraph-Loginreason: AUTHENTICATION_DENIED header to catch login failures 2015-02-19 10:05:51 -08:00
coryb 28bd1dffa5 Merge pull request #7 from jaybuff/empty-projects
validate project
2015-02-18 21:06:55 -08:00
Jay Buffington 5f7b46173a project should always be uppercase
Jira docs say as much:
https://confluence.atlassian.com/display/JIRA/Changing+the+Project+Key+Format#ChangingtheProjectKeyFormat-prerequisites
2015-02-18 19:26:36 -08:00
Jay Buffington 39a194b858 if response is 400, check json for errorMessages and log them 2015-02-18 17:39:37 -08:00
Jay Buffington 4b6329597b validate project 2015-02-18 17:32:56 -08:00
6 changed files with 111 additions and 17 deletions
+13 -4
View File
@@ -8,20 +8,24 @@ jira ls -p GOJIRA # list all unresolved issues for project
jira ls -p GOJIRA -a mothra # as above also assigned to user mothra
jira ls -p GOJIRA -w mothra # lists GOJIRA unresolved issues watched by user mothra
jira ls -p GOJIRA -r mothra # list GOJIRA unresolved issues reported by user mothra
jira ls -t table -p GOJIRA # list all unresolved issues in pretty table output
jira view GOJIRA-321 # print Issue using "view" template
jira GOJIRA-321 # same as above
jira edit GOJIRA-321 # open up the issue in an editor, when you exit the editor
# the issue will post the updates to the server
jira edit GOJIRA-321 # open up the issue in an editor, when you exit the
# editor the issue will post the updates to the server
# edit the issue, using the overirdes on the command line, skip the interactive editor:
jira edit GOJIRA-321 --noedit -o assignee=mothra -o comment="mothra, please take care of this." -o priority=Major
jira edit GOJIRA-321 --noedit \
-o assignee=mothra \
-o comment="mothra, please take care of this." \
-o priority=Major
jira create -p GOJIRA # create new "Bug" type issue for project GOJIRA
jira create -p GOJIRA -i Task # create new Task type issue
jira trans close GOJIRA-321 # close issue, with interactive editor to be able to set other fields
jira trans close GOJIRA-321 # close issue, with interactive editor to set fields
jira close GOJIRA-321 --edit # same as above
# close the issue, set the resolution, and skip interactive editor:
@@ -47,9 +51,14 @@ jira ls # list all unresolved issues for project
jira ls -a mothra # as above also assigned to user mothra
jira ls -w mothra # lists GOJIRA unresolved issues watched by user mothra
jira ls -r mothra # list GOJIRA unresolved issues reported by user mothra
jira ls -t table # list all unresolved issues in pretty table output
jira create # create new "Bug" type issue for project GOJIRA
jira create -i Task # create new Task type issue
# make the table template your default "list" template:
jira export-templates -t table
mv $HOME/.jira.d/templates/table $HOME/.jira.d/templates/list
```
## Download
+4
View File
@@ -32,6 +32,10 @@ func New(opts map[string]string) *Cli {
endpoint, _ := opts["endpoint"]
url, _ := url.Parse(strings.TrimRight(endpoint, "/"))
if project, ok := opts["project"]; ok {
opts["project"] = strings.ToUpper(project)
}
cli := &Cli{
endpoint: url,
opts: opts,
+27 -6
View File
@@ -27,14 +27,22 @@ func (c *Cli) CmdLogin() error {
// 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)
err := fmt.Errorf("Authenticaion Failed: %s", reason)
log.Error("%s", err)
return err
}
log.Error("Authentication Failead: Unknown")
return fmt.Errorf("Authentication Failead")
err := fmt.Errorf("Authentication Failed: Unknown Reason")
log.Error("%s", err)
return err
}
if resp.StatusCode != 200 {
} else if resp.StatusCode == 200 {
// https://confluence.atlassian.com/display/JIRA043/JIRA+REST+API+%28Alpha%29+Tutorial#JIRARESTAPI%28Alpha%29Tutorial-CAPTCHAs
// probably bad password, try again
if reason := resp.Header.Get("X-Seraph-Loginreason"); reason == "AUTHENTICATION_DENIED" {
log.Warning("Authentication Failed: %s", reason)
continue
}
} else {
log.Warning("Login failed")
continue
}
@@ -220,6 +228,11 @@ func (c *Cli) CmdCreateMeta(project string, issuetype string) error {
}
if val, ok := data.(map[string]interface{})["projects"]; ok {
if len(val.([]interface{})) == 0 {
err = fmt.Errorf("Project '%s' or issuetype '%s' unknown. Unable to createmeta.", project, issuetype)
log.Error("%s", err)
return err
}
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
data = val.([]interface{})[0]
}
@@ -253,6 +266,11 @@ func (c *Cli) CmdCreate(project string, issuetype string) error {
issueData["overrides"].(map[string]string)["issuetype"] = issuetype
if val, ok := data.(map[string]interface{})["projects"]; ok {
if len(val.([]interface{})) == 0 {
err = fmt.Errorf("Project '%s' or issuetype '%s' unknown. Unable to create issue.", project, issuetype)
log.Error("%s", err)
return err
}
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
issueData["meta"] = val.([]interface{})[0]
}
@@ -551,6 +569,9 @@ func (c *Cli) CmdExportTemplates() error {
}
for name, template := range all_templates {
if wanted, ok := c.opts["template"]; ok && wanted != name {
continue
}
templateFile := fmt.Sprintf("%s/%s", dir, name)
if _, err := os.Stat(templateFile); err == nil {
log.Warning("Skipping %s, already exists", templateFile)
+8
View File
@@ -8,6 +8,7 @@ var all_templates = map[string]string{
"createmeta": default_debug_template,
"issuelinktypes": default_debug_template,
"list": default_list_template,
"table": default_table_template,
"view": default_view_template,
"edit": default_edit_template,
"transitions": default_transitions_template,
@@ -21,6 +22,13 @@ const default_debug_template = "{{ . | toJson}}\n"
const default_list_template = "{{ range .issues }}{{ .key | append \":\" | printf \"%-12s\"}} {{ .fields.summary }}\n{{ end }}"
const default_table_template =`+{{ "-" | rep 16 }}+{{ "-" | rep 57 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
| {{ "Issue" | printf "%-14s" }} | {{ "Summary" | printf "%-55s" }} | {{ "Priority" | printf "%-12s" }} | {{ "Status" | printf "%-12s" }} | {{ "Age" | printf "%-10s" }} | {{ "Reporter" | printf "%-12s" }} | {{ "Assignee" | printf "%-12s" }} |
+{{ "-" | rep 16 }}+{{ "-" | rep 57 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
{{ range .issues }}| {{ .key | printf "%-14s"}} | {{ .fields.summary | abbrev 55 | printf "%-55s" }} | {{.fields.priority.name | printf "%-12s" }} | {{.fields.status.name | printf "%-12s" }} | {{.fields.created | age | printf "%-10s" }} | {{.fields.reporter.name | printf "%-12s"}} | {{if .fields.assignee }}{{.fields.assignee.name | printf "%-12s" }}{{else}}<unassigned>{{end}} |
{{ end }}+{{ "-" | rep 16 }}+{{ "-" | rep 57 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
`
const default_view_template = `issue: {{ .key }}
created: {{ .fields.created }}
status: {{ .fields.status.name }}
+54 -2
View File
@@ -13,6 +13,7 @@ import (
"os"
"strings"
"text/template"
"time"
)
func FindParentPaths(fileName string) []string {
@@ -62,6 +63,28 @@ func readFile(file string) string {
return string(bytes)
}
func fuzzyAge(start string) (string, error) {
if t, err := time.Parse("2006-01-02T15:04:05.000-0700", start); err != nil {
return "", err
} else {
delta := time.Now().Sub(t)
if delta.Minutes() < 2 {
return "a minute", nil
} else if dm := delta.Minutes(); dm < 45 {
return fmt.Sprintf("%d minutes", int(dm)), nil
} else if dm := delta.Minutes(); dm < 90 {
return "an hour", nil
} else if dh := delta.Hours(); dh < 24 {
return fmt.Sprintf("%d hours", int(dh)), nil
} else if dh := delta.Hours(); dh < 48 {
return "a day", nil
} else {
return fmt.Sprintf("%d days", int(delta.Hours() / 24)), nil
}
}
return "unknown", nil
}
func runTemplate(templateContent string, data interface{}, out io.Writer) error {
if out == nil {
@@ -100,6 +123,25 @@ func runTemplate(templateContent string, data interface{}, out io.Writer) error
"split": func(sep string, content string) []string {
return strings.Split(content, sep)
},
"abbrev": func(max int, content string) string {
if len(content) > max {
var buffer bytes.Buffer
buffer.WriteString(content[:max-3])
buffer.WriteString("...")
return buffer.String()
}
return content
},
"rep": func(count int, content string) string {
var buffer bytes.Buffer
for i := 0; i < count; i += 1 {
buffer.WriteString(content)
}
return buffer.String()
},
"age": func(content string) (string, error) {
return fuzzyAge(content)
},
}
if tmpl, err := template.New("template").Funcs(funcs).Parse(templateContent); err != nil {
log.Error("Failed to parse template: %s", err)
@@ -116,9 +158,19 @@ func runTemplate(templateContent string, data interface{}, out io.Writer) error
func responseToJson(resp *http.Response, err error) (interface{}, error) {
if err != nil {
return nil, err
} else {
return jsonDecode(resp.Body), nil
}
data := jsonDecode(resp.Body)
if resp.StatusCode == 400 {
if val, ok := data.(map[string]interface{})["errorMessages"]; ok {
for _,errMsg := range val.([]interface{}) {
log.Error("%s", errMsg)
}
}
}
return data, nil
}
func jsonDecode(io io.Reader) interface{} {
+5 -5
View File
@@ -19,7 +19,7 @@ func main() {
home := os.Getenv("HOME")
usage := fmt.Sprintf(`
Usage:
jira [-v ...] [-u USER] [-e URI] [-t FILE] (ls|list) ( [-q JQL] | [-p PROJECT] [-c COMPONENT] [-a ASSIGNEE] [-i ISSUETYPE] [-w WATCHER] [-r REPORTER])
jira [-v ...] [-u USER] [-e URI] [-t FILE] (ls|list) ( [-q JQL] | [-p PROJECT] [-c COMPONENT] [-a ASSIGNEE] [-i ISSUETYPE] [-w WATCHER] [-r REPORTER]) [-f FIELDS]
jira [-v ...] [-u USER] [-e URI] [-b] [-t FILE] view ISSUE
jira [-v ...] [-u USER] [-e URI] [-b] [-t FILE] edit ISSUE [--noedit] [-m COMMENT] [-o KEY=VAL]...
jira [-v ...] [-u USER] [-e URI] [-b] [-t FILE] create [--noedit] [-p PROJECT] [-i ISSUETYPE] [-o KEY=VAL]...
@@ -43,7 +43,7 @@ Usage:
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] [-b] [-t FILE] transitions ISSUE
jira [-v ...] export-templates [-d DIR]
jira [-v ...] export-templates [-d DIR] [-t template]
jira [-v ...] [-u USER] [-e URI] (b|browse) ISSUE
jira [-v ...] [-u USER] [-e URI] [-t FILE] login
jira [-v ...] [-u USER] [-e URI] [-b] [-t FILE] ISSUE
@@ -61,7 +61,7 @@ Command Options:
-b --browse Open your browser to the Jira issue
-c --component=COMPONENT Component to Search for
-d --directory=DIR Directory to export templates to (default: %s)
-f --queryfields Fields that are used in "list" template: (default: summary)
-f --queryfields=FIELDS Fields that are used in "list" template: (default: summary,created,priority,status,reporter,assignee)
-i --issuetype=ISSUETYPE Jira Issue Type (default: Bug)
-m --comment=COMMENT Comment message for transition
-o --override=KEY:VAL Set custom key/value pairs
@@ -72,7 +72,7 @@ Command Options:
or Watcher to search for
`, user, fmt.Sprintf("%s/.jira.d/templates", home), user)
args, err := docopt.Parse(usage, nil, true, "0.0.2", false, false)
args, err := docopt.Parse(usage, nil, true, "0.0.4", false, false)
if err != nil {
log.Error("Failed to parse options: %s", err)
os.Exit(1)
@@ -133,7 +133,7 @@ Command Options:
opts["user"] = user
}
if _, ok := opts["queryfields"]; !ok {
opts["queryfields"] = "summary"
opts["queryfields"] = "summary,created,priority,status,reporter,assignee"
}
if _, ok := opts["directory"]; !ok {
opts["directory"] = fmt.Sprintf("%s/.jira.d/templates", home)