Compare commits

...

39 Commits

Author SHA1 Message Date
Cory Bennett 66596f3aa5 update Changelog 2015-07-01 10:22:06 -07:00
Cory Bennett 3a68ccdd3f fix the "all" build 2015-07-01 10:20:48 -07:00
Cory Bennett 91c57d496c add cross-compile setup task 2015-07-01 09:13:03 -07:00
Cory Bennett 87ab7291e1 udpate version 2015-07-01 09:12:40 -07:00
Cory Bennett b72040bfd4 Merge branch 'master' of github.com:Netflix-Skunkworks/go-jira 2015-07-01 08:50:40 -07:00
coryb 2dcc840a06 Merge pull request #9 from nelfin/quickfix/take-user
fix "take" command not honouring user option
2015-06-30 23:29:09 -07:00
Andrew Haigh 10c4aef671 fix "take" command not honouring user option
"take" was simply a partial on "assign", but accidentally used the value
of $USER from the environment rather than `opts["user"]`, preventing the
user from overriding this value in config.yml or as a command-line
argument.
2015-07-01 16:21:26 +10:00
Cory Bennett b5643e545b fix typo 2015-03-02 14:54:24 -08:00
Cory Bennett fc00743095 strip binaries when building "all" 2015-03-02 14:34:38 -08:00
Cory Bennett 6a3e2aa4d4 updating 2015-02-27 17:52:07 -08:00
Cory Bennett abc3953448 bump version 2015-02-27 17:50:08 -08:00
Cory Bennett 52eb7f4ed7 allow --sort= to disable sort override 2015-02-24 17:49:21 -08:00
Cory Bennett a5c7a133c0 fix default JIRA_OPERATION env variable 2015-02-24 17:48:59 -08:00
Cory Bennett f42d0b6366 automatically close duplicate issues with "Duplicate" resolution 2015-02-23 12:09:40 -08:00
Cory Bennett 8040746bcf set JIRA_OPERATION to "view" when no operation used (ie: jira GOJIRA-123) 2015-02-23 12:08:54 -08:00
Cory Bennett 90a8ee7c33 add --sort option to "list" command 2015-02-23 12:08:33 -08:00
Cory Bennett 74ae039c37 adding changlog 2015-02-21 23:36:42 -08:00
Cory Bennett 20a16e2d0c bump version 2015-02-21 23:27:53 -08:00
Cory Bennett 4c23867836 fix typo 2015-02-21 21:35:31 -08:00
Cory Bennett b2edc436bc update README.md 2015-02-21 21:32:42 -08:00
Cory Bennett cc5878fabc handle editor having arguments 2015-02-21 21:15:02 -08:00
Cory Bennett 60f07bcdd6 add more template error handling 2015-02-20 15:03:40 -08:00
Cory Bennett 400b53acc8 gofmt 2015-02-20 14:39:01 -08:00
Cory Bennett 18cfbb337e gofmt 2015-02-20 14:37:55 -08:00
Cory Bennett 923b7f6cc7 allow create template to specify defalt watchers with -o watchers=... 2015-02-20 14:29:33 -08:00
Cory Bennett f0f620f739 update README.md 2015-02-20 14:29:19 -08:00
Cory Bennett 71f4a8012d if config files are executable then run them and parse the output 2015-02-20 13:37:24 -08:00
Cory Bennett 4532f75db3 update usage 2015-02-19 18:03:33 -08:00
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
8 changed files with 345 additions and 54 deletions
+42
View File
@@ -0,0 +1,42 @@
# Changelog
## 0.0.7 - 2015-07-01
* fix "take" command not honouring user option [Andrew Haigh] [[8f1d2b9](https://github.com/Netflix-Skunkworks/go-jira/commit/8f1d2b9)]
* fix typo [Cory Bennett] [[06f57fe](https://github.com/Netflix-Skunkworks/go-jira/commit/06f57fe)]
## 0.0.6 - 2015-02-27
* allow --sort= to disable sort override [Cory Bennett] [[701f091](https://github.com/Netflix-Skunkworks/go-jira/commit/701f091)]
* fix default JIRA_OPERATION env variable [Cory Bennett] [[82fd9b9](https://github.com/Netflix-Skunkworks/go-jira/commit/82fd9b9)]
* automatically close duplicate issues with "Duplicate" resolution [Cory Bennett] [[ebf1700](https://github.com/Netflix-Skunkworks/go-jira/commit/ebf1700)]
* set JIRA_OPERATION to "view" when no operation used (ie: jira GOJIRA-123) [Cory Bennett] [[050848a](https://github.com/Netflix-Skunkworks/go-jira/commit/050848a)]
* add --sort option to "list" command [Cory Bennett] [[f359030](https://github.com/Netflix-Skunkworks/go-jira/commit/f359030)]
## 0.0.5 - 2015-02-21
* handle editor having arguments [Cory Bennett] [[7186fb3](https://github.com/Netflix-Skunkworks/go-jira/commit/7186fb3)]
* add more template error handling [Cory Bennett] [[3e6f2b3](https://github.com/Netflix-Skunkworks/go-jira/commit/3e6f2b3)]
* allow create template to specify defalt watchers with -o watchers=... [Cory Bennett] [[4db2e4e](https://github.com/Netflix-Skunkworks/go-jira/commit/4db2e4e)]
* if config files are executable then run them and parse the output [Cory Bennett] [[7a2f7f5](https://github.com/Netflix-Skunkworks/go-jira/commit/7a2f7f5)]
## 0.0.4 - 2015-02-19
* add --template option to export-templates to export a single template [Cory Bennett] [[343fbb6](https://github.com/Netflix-Skunkworks/go-jira/commit/343fbb6)]
* add "table" template to be used with "list" command [Cory Bennett] [[8954ec1](https://github.com/Netflix-Skunkworks/go-jira/commit/8954ec1)]
## 0.0.3 - 2015-02-19
* [issue [#8](https://github.com/Netflix-Skunkworks/go-jira/issues/8)] detect X-Seraph-Loginreason: AUTHENTICATION_DENIED header to catch login failures [Cory Bennett] [[2dcf665](https://github.com/Netflix-Skunkworks/go-jira/commit/2dcf665)]
* project should always be uppercase [Jay Buffington] [[1b69d12](https://github.com/Netflix-Skunkworks/go-jira/commit/1b69d12)]
* if response is 400, check json for errorMessages and log them [Jay Buffington] [[4924dfa](https://github.com/Netflix-Skunkworks/go-jira/commit/4924dfa)]
* validate project [Jay Buffington] [[dc5ae42](https://github.com/Netflix-Skunkworks/go-jira/commit/dc5ae42)]
## 0.0.2 - 2015-02-18
* add missing --override options on transition command
* add browse command
## 0.0.1 - 2015-02-18
* Initial experimental release
+31 -2
View File
@@ -18,13 +18,42 @@ export GOPATH=$(shell pwd)
build:
cd src/github.com/Netflix-Skunkworks/go-jira/jira; \
go install -v
go get -v
cross-setup:
for p in $(PLATFORMS); do \
echo "Building for $$p"; \
cd $(GOROOT)/src && sudo GOOS=$${p/-*/} GOARCH=$${p/*-/} bash ./make.bash --no-clean; \
done
all:
rm -rf $(DIST); \
mkdir -p $(DIST); \
cd src/github.com/Netflix-Skunkworks/go-jira/jira; \
go get -d; \
for p in $(PLATFORMS); do \
echo "Building for $$p"; \
GOOS=$${p/-*/} GOARCH=$${p/*-/} go build -v -o $(DIST)/jira-$$p; \
GOOS=$${p/-*/} GOARCH=$${p/*-/} go build -v -ldflags -s -o $(DIST)/jira-$$p; \
done
fmt:
gofmt -s -w jira
CURVER := $(shell grep '\#\#' CHANGELOG.md | awk '{print $$2; exit}')
NEWVER := $(shell awk -F'"' '/docopt.Parse/{print $$2}' jira/main.go)
TODAY := $(shell date +%Y-%m-%d)
changes:
@git log --pretty=format:"* %s [%cn] [%h]" --no-merges ^$(CURVER) HEAD jira | grep -v gofmt | grep -v "bump version"
update-changelog:
@echo "# Changelog" > CHANGELOG.md.new; \
echo >> CHANGELOG.md.new; \
echo "## $(NEWVER) - $(TODAY)" >> CHANGELOG.md.new; \
echo >> CHANGELOG.md.new; \
$(MAKE) changes | \
perl -pe 's{\[([a-f0-9]+)\]}{[[$$1](https://github.com/Netflix-Skunkworks/go-jira/commit/$$1)]}g' | \
perl -pe 's{\#(\d+)}{[#$$1](https://github.com/Netflix-Skunkworks/go-jira/issues/$$1)}g' >> CHANGELOG.md.new; \
tail +2 CHANGELOG.md >> CHANGELOG.md.new; \
mv CHANGELOG.md.new CHANGELOG.md
+55 -16
View File
@@ -1,5 +1,5 @@
# go-jira
simple jira command line client in Go
simple command line client for Atlassian's Jira service written in Go
## Synopsis
@@ -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
@@ -108,6 +117,36 @@ endpoint: https://jira.mycompany.com
EOM
```
### Dynamic Configuration
If the **.jira.d/config.yml** file is executable, then **go-jira** will attempt to execute the file and use the stdout for configuration. You can use this to customize templates or other overrides depending on what type of operation you are running. For example if you would like to use the "table" template when ever you run `jira ls`, then you can create a template like this:
```sh
#!/bin/sh
echo "endpoint: https://jira.mycompany.com"
echo "editor: emacs -nw"
case $JIRA_OPERATION in
list)
echo "template: table";;
esac
```
Or if you always set the same overrides when you create an issue for your project you can do something like this:
```sh
#!/bin/sh
echo "project: GOJIRA"
case $JIRA_OPERATION in
create)
echo "assignee: $USER"
echo "watchers: mothra"
;;
esac
```
### Editing
When you run command like `jira edit` it will open up your favorite editor with the templatized output so you can quickly edit. When the editor
@@ -132,21 +171,20 @@ hard-coded templates with `jira export-templates` which will write them to **~/.
```
Usage:
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]...
jira [-v ...] [-u USER] [-e URI] [-b] DUPLICATE dups ISSUE
jira [-v ...] [-u USER] [-e URI] [-b] BLOCKER blocks ISSUE
jira [-v ...] [-u USER] [-e URI] [-b] watch ISSUE [-w WATCHER]
jira [-v ...] [-u USER] [-e URI] [-b] [-t FILE] (trans|transition) TRANSITION ISSUE [-m COMMENT] [--noedit]
jira [-v ...] [-u USER] [-e URI] [-b] ack ISSUE [-m COMMENT] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] close ISSUE [-m COMMENT] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] resolve ISSUE [-m COMMENT] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] reopen ISSUE [-m COMMENT] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] start ISSUE [-m COMMENT] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] stop ISSUE [-m COMMENT] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] [-t FILE] (trans|transition) TRANSITION ISSUE [-m COMMENT] [-o KEY=VAL] [--noedit]
jira [-v ...] [-u USER] [-e URI] [-b] ack ISSUE [-m COMMENT] [-o KEY=VAL] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] close ISSUE [-m COMMENT] [-o KEY=VAL] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] resolve ISSUE [-m COMMENT] [-o KEY=VAL] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] reopen ISSUE [-m COMMENT] [-o KEY=VAL] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] start ISSUE [-m COMMENT] [-o KEY=VAL] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] stop ISSUE [-m COMMENT] [-o KEY=VAL] [--edit]
jira [-v ...] [-u USER] [-e URI] [-b] [-t FILE] comment ISSUE [-m COMMENT]
jira [-v ...] [-u USER] [-e URI] [-b] take ISSUE
jira [-v ...] [-u USER] [-e URI] [-b] (assign|give) ISSUE ASSIGNEE
@@ -157,7 +195,8 @@ 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
@@ -174,7 +213,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: /Users/cbennett/.jira.d/templates)
-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
+11 -4
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/kballard/go-shellquote"
"github.com/op/go-logging"
"gopkg.in/yaml.v2"
"io/ioutil"
@@ -32,6 +33,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,
@@ -240,11 +245,13 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
if val, ok := c.opts["edit"]; ok && val == "false" {
editing = false
}
for true {
if editing {
log.Debug("Running: %s %s", editor, tmpFileName)
cmd := exec.Command(editor, tmpFileName)
shell, _ := shellquote.Split(editor)
shell = append(shell, tmpFileName)
log.Debug("Running: %#v", shell)
cmd := exec.Command(shell[0], shell[1:]...)
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)
@@ -281,7 +288,7 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
if _, ok := templateData["meta"]; ok {
mf := templateData["meta"].(map[string]interface{})["fields"]
if f, ok := edited["fields"].(map[string]interface{}); ok {
for k, _ := range f {
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)
+40 -14
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
}
@@ -90,12 +98,17 @@ func (c *Cli) CmdList() error {
if reporter, ok := c.opts["reporter"]; ok {
qbuff.WriteString(fmt.Sprintf(" AND reporter = '%s'", reporter))
}
if sort, ok := c.opts["sort"]; ok && sort != "" {
qbuff.WriteString(fmt.Sprintf(" ORDER BY %s", sort ))
}
query = qbuff.String()
}
fields := make([]string,0)
fields := make([]string, 0)
if qf, ok := c.opts["queryfields"]; ok {
fields = strings.Split(qf,",")
fields = strings.Split(qf, ",")
} else {
fields = append(fields, "summary")
}
@@ -104,7 +117,7 @@ func (c *Cli) CmdList() error {
"jql": query,
"startAt": "0",
"maxResults": "500",
"fields": fields,
"fields": fields,
})
if err != nil {
return err
@@ -220,6 +233,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 +271,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]
}
@@ -279,7 +302,7 @@ func (c *Cli) CmdCreate(project string, issuetype string) error {
key := json.(map[string]interface{})["key"]
c.Browse(key.(string))
fmt.Printf("OK %s %s/browse/%s\n", key, c.endpoint, key)
}
return nil
} else {
@@ -450,7 +473,7 @@ func (c *Cli) CmdTransition(issue string, trans string) error {
}
return nil
}
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 {
@@ -462,9 +485,9 @@ func (c *Cli) CmdTransition(issue string, trans string) error {
issueData["overrides"] = c.opts
issueData["transition"] = map[string]interface{}{
"name": transName,
"id": transId,
};
"id": transId,
}
return c.editTemplate(
c.getTemplate("transition"),
fmt.Sprintf("%s-trans-%s-", issue, trans),
@@ -551,6 +574,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)
+12 -3
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" }} | {{if .fields.reporter}}{{ .fields.reporter.name | printf "%-12s"}}{{else}}<unassigned>{{end}} | {{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 }}
@@ -29,7 +37,7 @@ project: {{ .fields.project.key }}
components: {{ range .fields.components }}{{ .name }} {{end}}
issuetype: {{ .fields.issuetype.name }}
assignee: {{ if .fields.assignee }}{{ .fields.assignee.name }}{{end}}
reporter: {{ .fields.reporter.name }}
reporter: {{ if .fields.reporter }}{{ .fields.reporter.name }}{{end}}
watchers: {{ range .fields.customfield_10110 }}{{ .name }} {{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}}
@@ -55,7 +63,7 @@ fields:
assignee:
name: {{ if .overrides.assignee }}{{.overrides.assignee}}{{else}}{{if .fields.assignee }}{{ .fields.assignee.name }}{{end}}{{end}}
reporter:
name: {{ or .overrides.reporter .fields.reporter.name }}
name: {{ if .overrides.reporter }}{{ .overrides.reporter }}{{else if .fields.reporter}}{{ .fields.reporter.name }}{{end}}
# watchers
customfield_10110: {{ range .fields.customfield_10110 }}
- name: {{ .name }}{{end}}{{if .overrides.watcher}}
@@ -88,7 +96,8 @@ const default_create_template = `fields:
reporter:
name: {{ or .overrides.reporter .overrides.user }}
# watchers
customfield_10110:
customfield_10110: {{ range split "," (or .overrides.watchers "")}}
- name: {{.}}{{end}}
- name:
`
+53 -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,18 @@ 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{} {
+101 -13
View File
@@ -1,6 +1,7 @@
package main
import (
"bytes"
"fmt"
"github.com/Netflix-Skunkworks/go-jira/jira/cli"
"github.com/docopt/docopt-go"
@@ -8,6 +9,7 @@ import (
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"os/exec"
"strings"
)
@@ -19,7 +21,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] [-s ORDER]
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 +45,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,18 +63,19 @@ 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
-p --project=PROJECT Project to Search for
-q --query=JQL Jira Query Language expression for the search
-r --reporter=USER Reporter to search for
-s --sort=ORDER For list operations, sort issues (default: priority asc, created)
-w --watcher=USER Watcher to add to issue (default: %s)
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.7", false, false)
if err != nil {
log.Error("Failed to parse options: %s", err)
os.Exit(1)
@@ -95,6 +98,8 @@ Command Options:
log.Info("Args: %v", args)
populateEnv(args)
opts := make(map[string]string)
loadConfigs(opts)
@@ -133,17 +138,20 @@ 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)
}
if _, ok := opts["sort"]; !ok {
opts["sort"] = "priority asc, created"
}
if _, ok := opts["endpoint"]; !ok {
log.Error("endpoint option required. Either use --endpoint or set a enpoint option in your ~/.jira.d/config.yml file")
os.Exit(1)
}
c := cli.New(opts)
log.Debug("opts: %s", opts)
@@ -174,9 +182,7 @@ Command Options:
opts["edit"] = "true"
}
} else {
if val, ok := opts["edit"]; ok && val == "true" {
opts["edit"] = "true"
} else {
if val, ok := opts["edit"]; ok && val != "true" {
opts["edit"] = "false"
}
}
@@ -218,10 +224,16 @@ Command Options:
args["ISSUE"].(string),
)
} else if validCommand("dups") {
err = c.CmdDups(
if err = c.CmdDups(
args["DUPLICATE"].(string),
args["ISSUE"].(string),
)
); err == nil {
opts["resolution"] = "Duplicate"
err = c.CmdTransition(
args["DUPLICATE"].(string),
"close",
)
}
} else if validCommand("watch") {
err = c.CmdWatch(
args["ISSUE"].(string),
@@ -255,7 +267,7 @@ Command Options:
setEditing(true)
err = c.CmdComment(args["ISSUE"].(string))
} else if validCommand("take") {
err = c.CmdAssign(args["ISSUE"].(string), user)
err = c.CmdAssign(args["ISSUE"].(string), opts["user"])
} else if validCommand("browse") || validCommand("b") {
opts["browse"] = "true"
err = c.Browse(args["ISSUE"].(string))
@@ -283,12 +295,88 @@ func parseYaml(file string, opts map[string]string) {
}
}
func populateEnv(args map[string]interface{}) {
foundOp := false
for key, val := range args {
if val != nil && strings.HasPrefix(key, "--") {
if key == "--override" {
for _, v := range val.([]string) {
if strings.Contains(v, "=") {
kv := strings.SplitN(v, "=", 2)
envName := fmt.Sprintf("JIRA_%s", strings.ToUpper(kv[0]))
os.Setenv(envName, kv[1])
} else {
log.Error("Malformed override, expected KEY=VALUE, got %s", v)
os.Exit(1)
}
}
} else {
envName := fmt.Sprintf("JIRA_%s", strings.ToUpper(key[2:]))
switch v := val.(type) {
case []string:
os.Setenv(envName, strings.Join(v, ","))
case string:
os.Setenv(envName, v)
case bool:
if v {
os.Setenv(envName, "1")
} else {
os.Setenv(envName, "0")
}
}
}
} else if val != nil {
// lower case strings are operations
if strings.ToLower(key) == key {
if key == "ls" && val.(bool) {
foundOp = true
os.Setenv("JIRA_OPERATION", "list")
} else if key == "b" && val.(bool) {
foundOp = true
os.Setenv("JIRA_OPERATION", "browse")
} else if key == "trans" && val.(bool) {
foundOp = true
os.Setenv("JIRA_OPERATION", "transition")
} else if key == "give" && val.(bool) {
foundOp = true
os.Setenv("JIRA_OPERATION", "assign")
} else if val.(bool) {
foundOp = true
os.Setenv("JIRA_OPERATION", key)
}
} else {
os.Setenv(fmt.Sprintf("JIRA_%s", key), val.(string))
}
}
}
if !foundOp {
os.Setenv("JIRA_OPERATION", "view")
}
}
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)
if stat, err := os.Stat(file); err == nil {
// check to see if config file is exectuable
if stat.Mode()&0111 == 0 {
parseYaml(file, opts)
} else {
log.Debug("Found Executable Config file: %s", file)
// it is executable, so run it and try to parse the output
cmd := exec.Command(file)
stdout := bytes.NewBufferString("")
cmd.Stdout = stdout
cmd.Stderr = bytes.NewBufferString("")
if err := cmd.Run(); err != nil {
log.Error("%s is exectuable, but it failed to execute: %s\n%s", file, err, cmd.Stderr)
os.Exit(1)
}
yaml.Unmarshal(stdout.Bytes(), &opts)
}
}
}
}