mirror of
https://github.com/Threnklyn/jira.git
synced 2026-05-20 13:13:27 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f95aa3d261 | |||
| 4d95bde10f | |||
| b35b8d1fd1 | |||
| fd4ec5e641 | |||
| 0b88d0ad97 | |||
| 8c07442645 | |||
| f3feff796f | |||
| 28bd1dffa5 | |||
| 5f7b46173a | |||
| 39a194b858 | |||
| 4b6329597b |
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user