mirror of
https://github.com/Threnklyn/jira.git
synced 2026-05-20 13:13:27 +02:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66596f3aa5 | |||
| 3a68ccdd3f | |||
| 91c57d496c | |||
| 87ab7291e1 | |||
| b72040bfd4 | |||
| 2dcc840a06 | |||
| 10c4aef671 | |||
| b5643e545b | |||
| fc00743095 | |||
| 6a3e2aa4d4 | |||
| abc3953448 | |||
| 52eb7f4ed7 | |||
| a5c7a133c0 | |||
| f42d0b6366 | |||
| 8040746bcf | |||
| 90a8ee7c33 | |||
| 74ae039c37 | |||
| 20a16e2d0c | |||
| 4c23867836 | |||
| b2edc436bc | |||
| cc5878fabc | |||
| 60f07bcdd6 | |||
| 400b53acc8 | |||
| 18cfbb337e | |||
| 923b7f6cc7 | |||
| f0f620f739 | |||
| 71f4a8012d | |||
| 4532f75db3 | |||
| f95aa3d261 | |||
| 4d95bde10f | |||
| b35b8d1fd1 | |||
| fd4ec5e641 | |||
| 0b88d0ad97 | |||
| 8c07442645 | |||
| f3feff796f | |||
| 28bd1dffa5 | |||
| 5f7b46173a | |||
| 39a194b858 | |||
| 4b6329597b |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user