mirror of
https://github.com/Threnklyn/jira.git
synced 2026-05-20 21:23:27 +02:00
Compare commits
89 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 | |||
| 03ad7ee9b2 | |||
| c1681b632c | |||
| af4952621b | |||
| 74178e917b | |||
| 56c610718d | |||
| 9d7272c45e | |||
| 94af4c46af | |||
| 8464ff1b41 | |||
| 87758ecc3b | |||
| 1cf15318a1 | |||
| 6e2ee2b865 | |||
| 6f0d1f5e44 | |||
| 4524d89435 | |||
| 1ac6929e56 | |||
| 45fb06f6bf | |||
| 9bf7533fc2 | |||
| e9c866d38b | |||
| a7399c7f48 | |||
| 9d6fdf73e5 | |||
| b5f62fb092 | |||
| b616e640dd | |||
| 6a9afae5b4 | |||
| 14a0ae1cc3 | |||
| 25539efedd | |||
| 421473140a | |||
| 9fb23d6e00 | |||
| d5ac5e677e | |||
| 306d66dba2 | |||
| eedbd94b98 | |||
| 798016de78 | |||
| 025c5edc6d | |||
| 3caf524d3e | |||
| 0806aa3202 | |||
| e1ee1fc29a | |||
| 2cdb1d3cf7 | |||
| f95baf7c1c | |||
| 5c3c02f8d8 | |||
| 76ec33e7e3 | |||
| 97ad931f79 | |||
| 4c1e0ec93e | |||
| 7697594fe2 | |||
| 60e4925fe4 | |||
| 48ee9ae8ba | |||
| 0010215242 | |||
| 44bc16b02e | |||
| 18f10fd125 | |||
| acbc24b209 | |||
| 1d96b5549e | |||
| bb6b7ee6af | |||
| 6936b27ea1 |
@@ -0,0 +1,7 @@
|
||||
bin/
|
||||
pkg/
|
||||
src/code.google.com/
|
||||
src/github.com/docopt/
|
||||
src/github.com/mgutz/
|
||||
src/github.com/op/
|
||||
src/gopkg.in/
|
||||
@@ -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
|
||||
@@ -0,0 +1,59 @@
|
||||
PLATFORMS= \
|
||||
freebsd-386 \
|
||||
freebsd-amd64 \
|
||||
freebsd-arm \
|
||||
linux-386 \
|
||||
linux-amd64 \
|
||||
linux-arm \
|
||||
openbsd-386 \
|
||||
openbsd-amd64 \
|
||||
windows-386 \
|
||||
windows-amd64 \
|
||||
darwin-386 \
|
||||
darwin-amd64 \
|
||||
$(NULL)
|
||||
|
||||
DIST=$(shell pwd)/dist
|
||||
export GOPATH=$(shell pwd)
|
||||
|
||||
build:
|
||||
cd src/github.com/Netflix-Skunkworks/go-jira/jira; \
|
||||
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 -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,2 +1,225 @@
|
||||
# go-jira
|
||||
simple jira command line client in Go
|
||||
simple command line client for Atlassian's Jira service written in Go
|
||||
|
||||
## Synopsis
|
||||
|
||||
```bash
|
||||
jira ls -p GOJIRA # list all unresolved issues for project GOJRIA
|
||||
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
|
||||
|
||||
# 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 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 set fields
|
||||
jira close GOJIRA-321 --edit # same as above
|
||||
|
||||
# close the issue, set the resolution, and skip interactive editor:
|
||||
jira trans close GOJIRA-321 -o resolution="Won't Fix" --noedit
|
||||
# same as above
|
||||
jira close GOJIRA-321 -o resolution="Won't Fix"
|
||||
|
||||
jira repopen GOJIRA-321 -m "reopening" # reopen issue
|
||||
|
||||
jira watch GOJIRA-321 # add self as watcher to the issue
|
||||
|
||||
jira comment GOJIRA-321 -m "done yet?" # add comment to the issue
|
||||
|
||||
jira take GOJIRA-321 # assign issue to self
|
||||
|
||||
jira give GOJIRA-321 mothra # assign issue to user mothra
|
||||
|
||||
# create local project config to set defaults
|
||||
mkdir .jira.d
|
||||
echo "project: GOJIRA" > .jira.d/config.yml
|
||||
|
||||
jira ls # list all unresolved issues for project GOJRIA
|
||||
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
|
||||
|
||||
You can download one of the pre-built binaries for **go-jira** [here](https://github.com/Netflix-Skunkworks/go-jira/releases).
|
||||
|
||||
## Build
|
||||
|
||||
* **NOTE** You will need **`go-1.4.1`** minimum
|
||||
|
||||
* If you do not have a **GOPATH** setup, these are simple build steps:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:Netflix-Skunkworks/go-jira.git
|
||||
cd go-jira
|
||||
export GOPATH=$(pwd)
|
||||
export GOBIN=$GOPATH/bin
|
||||
export PATH=$GOBIN:$PATH
|
||||
cd src/github.com/Netflix-Skunkworks/go-jira/jira
|
||||
go get -v
|
||||
```
|
||||
|
||||
* If you do have a **GOPATH** setup, these are the standard steps to build:
|
||||
|
||||
```
|
||||
cd $GOPATH
|
||||
git clone git@github.com:Netflix-Skunkworks/go-jira.git src/github.com/Netflix-Skunkworks/go-jira
|
||||
cd src/github.com/Netflix-Skunkworks/go-jira/jira
|
||||
go get -v
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
**go-jira** uses a configuration hierarchy. When loading the configuration from disk it will recursively look through
|
||||
all parent directories in your current path looking for a **.jira.d** directory. If your current directory is not
|
||||
a child directory of your homedir, then your homedir will also be inspected for a **.jira.d** directory. From all of **.jira.d** directories
|
||||
discovered **go-jira** will load a **config.yml** if found. The configuration properties found in a file closests to your current working directory
|
||||
will have precedence. Properties overriden with command line options will have final precedence.
|
||||
|
||||
The complicated configuration heirarchy is used because **go-jira** attempts to be context aware. For example, if you are working on a "foo" project and
|
||||
you `cd` into your project workspace, wouldn't it be nice if `jira ls` automatically knew to list only issues related to the "foo" project? Likewise when you
|
||||
`cd` to the "bar" project then `jira ls` should only list issues related to "bar" project. You can do this with by creating a configuration under your project
|
||||
workspace at **./.jira.d/config.yml** that looks like:
|
||||
|
||||
```
|
||||
project: foo
|
||||
```
|
||||
|
||||
You will need to specify your local jira endpoint first, typically in your homedir like:
|
||||
|
||||
```bash
|
||||
mkdir ~/.jira.d
|
||||
|
||||
cat <<EOM >~/.jira.d/config.yml
|
||||
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
|
||||
closes **go-jira** will submit the completed form. The order which **go-jira** attempts to determine your prefered editor is:
|
||||
|
||||
* **editor** property in any config.yml file
|
||||
* **JIRA_EDITOR** environment variable
|
||||
* **EDITOR** environment variable
|
||||
* vim
|
||||
|
||||
### Templates
|
||||
|
||||
**go-jira** has the ability to customize most output (and editor input) via templates. There are default templates available for all operations,
|
||||
which may or may not work for your actual jira implementation. Jira is endlessly customizable, so it is hard to provide default templates
|
||||
that will work for all issue types.
|
||||
|
||||
When running a command like `jira edit` it will look through the current directory hierarchy trying to find a file that matches **.jira.d/templates/edit**,
|
||||
if found it will use that file as the template, otherwise it will use the default **edit** template hard-coded into **go-jira**. You can export the default
|
||||
hard-coded templates with `jira export-templates` which will write them to **~/.jira.d/templates/**.
|
||||
|
||||
## 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]) [-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] [-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
|
||||
jira [-v ...] [-u USER] [-e URI] [-t FILE] fields
|
||||
jira [-v ...] [-u USER] [-e URI] [-t FILE] issuelinktypes
|
||||
jira [-v ...] [-u USER] [-e URI] [-b][-t FILE] transmeta ISSUE
|
||||
jira [-v ...] [-u USER] [-e URI] [-b] [-t FILE] editmeta 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] [-b] [-t FILE] transitions ISSUE
|
||||
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
|
||||
|
||||
General Options:
|
||||
-e --endpoint=URI URI to use for jira
|
||||
-h --help Show this usage
|
||||
-t --template=FILE Template file to use for output/editing
|
||||
-u --user=USER Username to use for authenticaion (default: cbennett)
|
||||
-v --verbose Increase output logging
|
||||
--version Show this version
|
||||
|
||||
Command Options:
|
||||
-a --assignee=USER Username assigned the issue
|
||||
-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 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
|
||||
-w --watcher=USER Watcher to add to issue (default: cbennett)
|
||||
or Watcher to search for
|
||||
```
|
||||
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/op/go-logging"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var log = logging.MustGetLogger("jira.cli")
|
||||
|
||||
type Cli struct {
|
||||
endpoint *url.URL
|
||||
opts map[string]string
|
||||
cookieFile string
|
||||
ua *http.Client
|
||||
}
|
||||
|
||||
func New(opts map[string]string) *Cli {
|
||||
homedir := os.Getenv("HOME")
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
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,
|
||||
cookieFile: fmt.Sprintf("%s/.jira.d/cookies.js", homedir),
|
||||
ua: &http.Client{Jar: cookieJar},
|
||||
}
|
||||
|
||||
cli.ua.Jar.SetCookies(url, cli.loadCookies())
|
||||
|
||||
return cli
|
||||
}
|
||||
|
||||
func (c *Cli) saveCookies(cookies []*http.Cookie) {
|
||||
// expiry in one week from now
|
||||
expiry := time.Now().Add(24 * 7 * time.Hour)
|
||||
for _, cookie := range cookies {
|
||||
cookie.Expires = expiry
|
||||
}
|
||||
|
||||
if currentCookies := c.loadCookies(); currentCookies != nil {
|
||||
currentCookiesByName := make(map[string]*http.Cookie)
|
||||
for _, cookie := range currentCookies {
|
||||
currentCookiesByName[cookie.Name] = cookie
|
||||
}
|
||||
|
||||
for _, cookie := range cookies {
|
||||
currentCookiesByName[cookie.Name] = cookie
|
||||
}
|
||||
|
||||
mergedCookies := make([]*http.Cookie, 0, len(currentCookiesByName))
|
||||
for _, v := range currentCookiesByName {
|
||||
mergedCookies = append(mergedCookies, v)
|
||||
}
|
||||
jsonWrite(c.cookieFile, mergedCookies)
|
||||
} else {
|
||||
jsonWrite(c.cookieFile, cookies)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) loadCookies() []*http.Cookie {
|
||||
bytes, err := ioutil.ReadFile(c.cookieFile)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
// dont load cookies if the file does not exist
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Failed to open %s: %s", c.cookieFile, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cookies := make([]*http.Cookie, 0)
|
||||
err = json.Unmarshal(bytes, &cookies)
|
||||
if err != nil {
|
||||
log.Error("Failed to parse json from file %s: %s", c.cookieFile, err)
|
||||
}
|
||||
log.Debug("Loading Cookies: %s", cookies)
|
||||
return cookies
|
||||
}
|
||||
|
||||
func (c *Cli) post(uri string, content string) (*http.Response, error) {
|
||||
return c.makeRequestWithContent("POST", uri, content)
|
||||
}
|
||||
|
||||
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)
|
||||
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) (resp *http.Response, err error) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
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)
|
||||
}
|
||||
|
||||
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(name 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 dflt, ok := all_templates[override]; ok {
|
||||
return dflt
|
||||
}
|
||||
}
|
||||
}
|
||||
if file, err := FindClosestParentPath(fmt.Sprintf(".jira.d/templates/%s", name)); err != nil {
|
||||
// create-bug etc are special, if we dont find it in the path
|
||||
// then just return a generic create template
|
||||
if strings.HasPrefix(name, "create-") {
|
||||
if file, err := FindClosestParentPath(".jira.d/templates/create"); err != nil {
|
||||
return all_templates["create"]
|
||||
} else {
|
||||
return readFile(file)
|
||||
}
|
||||
}
|
||||
return all_templates[name]
|
||||
} else {
|
||||
return readFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData map[string]interface{}, templateProcessor func(string) error) error {
|
||||
|
||||
tmpdir := fmt.Sprintf("%s/.jira.d/tmp", os.Getenv("HOME"))
|
||||
if err := mkdir(tmpdir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fh, err := ioutil.TempFile(tmpdir, tmpFilePrefix)
|
||||
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(template, templateData, 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editing := true
|
||||
if val, ok := c.opts["edit"]; ok && val == "false" {
|
||||
editing = false
|
||||
}
|
||||
|
||||
for true {
|
||||
if editing {
|
||||
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)
|
||||
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 editing && 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 editing && promptYN("edit again?", true) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if fixed, err := yamlFixup(edited); err != nil {
|
||||
return err
|
||||
} else {
|
||||
edited = fixed.(map[string]interface{})
|
||||
}
|
||||
|
||||
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 {
|
||||
if _, ok := mf.(map[string]interface{})[k]; !ok {
|
||||
err := fmt.Errorf("Field %s is not editable", k)
|
||||
log.Error("%s", err)
|
||||
if editing && promptYN("edit again?", true) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json, err := jsonEncode(edited)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := templateProcessor(json); err != nil {
|
||||
log.Error("%s", err)
|
||||
if editing && promptYN("edit again?", true) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) Browse(issue string) error {
|
||||
if val, ok := c.opts["browse"]; ok && val == "true" {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return exec.Command("open", fmt.Sprintf("%s/browse/%s", c.endpoint, issue)).Run()
|
||||
} else if runtime.GOOS == "linux" {
|
||||
return exec.Command("xdg-open", fmt.Sprintf("%s/browse/%s", c.endpoint, issue)).Run()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.google.com/p/gopass"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
// "github.com/kr/pretty"
|
||||
)
|
||||
|
||||
func (c *Cli) CmdLogin() error {
|
||||
uri := fmt.Sprintf("%s/rest/auth/1/session", c.endpoint)
|
||||
for true {
|
||||
req, _ := http.NewRequest("GET", uri, nil)
|
||||
user, _ := c.opts["user"]
|
||||
|
||||
prompt := fmt.Sprintf("Enter Password for %s: ", user)
|
||||
passwd, _ := gopass.GetPass(prompt)
|
||||
|
||||
req.SetBasicAuth(user, passwd)
|
||||
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 != "" {
|
||||
err := fmt.Errorf("Authenticaion Failed: %s", reason)
|
||||
log.Error("%s", err)
|
||||
return err
|
||||
}
|
||||
err := fmt.Errorf("Authentication Failed: Unknown Reason")
|
||||
log.Error("%s", err)
|
||||
return err
|
||||
|
||||
} 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
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdFields() error {
|
||||
log.Debug("fields called")
|
||||
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("fields"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdList() error {
|
||||
log.Debug("list called")
|
||||
|
||||
var query string
|
||||
var ok bool
|
||||
// project = BAKERY and status not in (Resolved, Closed)
|
||||
if query, ok = c.opts["query"]; !ok {
|
||||
qbuff := bytes.NewBufferString("resolution = unresolved")
|
||||
if project, ok := c.opts["project"]; !ok {
|
||||
err := fmt.Errorf("Missing required arguments, either 'query' or 'project' are required")
|
||||
log.Error("%s", err)
|
||||
return err
|
||||
} else {
|
||||
qbuff.WriteString(fmt.Sprintf(" AND project = '%s'", project))
|
||||
}
|
||||
|
||||
if component, ok := c.opts["component"]; ok {
|
||||
qbuff.WriteString(fmt.Sprintf(" AND component = '%s'", component))
|
||||
}
|
||||
|
||||
if assignee, ok := c.opts["assignee"]; ok {
|
||||
qbuff.WriteString(fmt.Sprintf(" AND assignee = '%s'", assignee))
|
||||
}
|
||||
|
||||
if issuetype, ok := c.opts["issuetype"]; ok {
|
||||
qbuff.WriteString(fmt.Sprintf(" AND issuetype = '%s'", issuetype))
|
||||
}
|
||||
|
||||
if watcher, ok := c.opts["watcher"]; ok {
|
||||
qbuff.WriteString(fmt.Sprintf(" AND watcher = '%s'", watcher))
|
||||
}
|
||||
|
||||
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)
|
||||
if qf, ok := c.opts["queryfields"]; ok {
|
||||
fields = strings.Split(qf, ",")
|
||||
} else {
|
||||
fields = append(fields, "summary")
|
||||
}
|
||||
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"jql": query,
|
||||
"startAt": "0",
|
||||
"maxResults": "500",
|
||||
"fields": fields,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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("list"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdView(issue string) error {
|
||||
log.Debug("view called")
|
||||
c.Browse(issue)
|
||||
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("view"), 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{})
|
||||
issueData["overrides"] = c.opts
|
||||
|
||||
return c.editTemplate(
|
||||
c.getTemplate("edit"),
|
||||
fmt.Sprintf("%s-edit-", issue),
|
||||
issueData,
|
||||
func(json string) error {
|
||||
resp, err := c.put(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 204 {
|
||||
c.Browse(issueData["key"].(string))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueData["key"], c.endpoint, issueData["key"])
|
||||
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
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdEditMeta(issue string) error {
|
||||
log.Debug("editMeta called")
|
||||
c.Browse(issue)
|
||||
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("editmeta"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdTransitionMeta(issue string) error {
|
||||
log.Debug("tranisionMeta called")
|
||||
c.Browse(issue)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", c.endpoint, issue)
|
||||
data, err := responseToJson(c.get(uri))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runTemplate(c.getTemplate("transmeta"), 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("issuetypes"), 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
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
return runTemplate(c.getTemplate("createmeta"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdTransitions(issue string) error {
|
||||
log.Debug("Transitions called")
|
||||
c.Browse(issue)
|
||||
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("transitions"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdCreate(project string, issuetype string) error {
|
||||
log.Debug("create 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
|
||||
}
|
||||
|
||||
issueData := make(map[string]interface{})
|
||||
issueData["overrides"] = c.opts
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
sanitizedType := strings.ToLower(strings.Replace(issuetype, " ", "", -1))
|
||||
return c.editTemplate(
|
||||
c.getTemplate(fmt.Sprintf("create-%s", sanitizedType)),
|
||||
fmt.Sprintf("create-%s-", sanitizedType),
|
||||
issueData,
|
||||
func(json string) error {
|
||||
log.Debug("JSON: %s", json)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue", c.endpoint)
|
||||
resp, err := c.post(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 201 {
|
||||
// response: {"id":"410836","key":"PROJ-238","self":"https://jira/rest/api/2/issue/410836"}
|
||||
if json, err := responseToJson(resp, nil); err != nil {
|
||||
return err
|
||||
} else {
|
||||
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 {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From POST")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdIssueLinkTypes() error {
|
||||
log.Debug("Transitions called")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issueLinkType", c.endpoint)
|
||||
data, err := responseToJson(c.get(uri))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runTemplate(c.getTemplate("issuelinktypes"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdBlocks(blocker string, issue string) error {
|
||||
log.Debug("blocks called")
|
||||
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"type": map[string]string{
|
||||
"name": "Depends", // TODO This is probably not constant across Jira installs
|
||||
},
|
||||
"inwardIssue": map[string]string{
|
||||
"key": issue,
|
||||
},
|
||||
"outwardIssue": map[string]string{
|
||||
"key": blocker,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issueLink", c.endpoint)
|
||||
resp, err := c.post(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == 201 {
|
||||
c.Browse(issue)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From POST")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdDups(duplicate string, issue string) error {
|
||||
log.Debug("dups called")
|
||||
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"type": map[string]string{
|
||||
"name": "Duplicate", // TODO This is probably not constant across Jira installs
|
||||
},
|
||||
"inwardIssue": map[string]string{
|
||||
"key": duplicate,
|
||||
},
|
||||
"outwardIssue": map[string]string{
|
||||
"key": issue,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issueLink", c.endpoint)
|
||||
resp, err := c.post(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == 201 {
|
||||
c.Browse(issue)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From POST")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdWatch(issue string, watcher string) error {
|
||||
log.Debug("watch called")
|
||||
|
||||
json, err := jsonEncode(watcher)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", c.endpoint, issue)
|
||||
resp, err := c.post(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == 204 {
|
||||
c.Browse(issue)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From POST")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdTransition(issue string, trans string) error {
|
||||
log.Debug("transition called")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", c.endpoint, issue)
|
||||
data, err := responseToJson(c.get(uri))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transitions := data.(map[string]interface{})["transitions"].([]interface{})
|
||||
var transId, transName string
|
||||
var transMeta map[string]interface{}
|
||||
found := make([]string, 0, len(transitions))
|
||||
for _, transition := range transitions {
|
||||
name := transition.(map[string]interface{})["name"].(string)
|
||||
id := transition.(map[string]interface{})["id"].(string)
|
||||
found = append(found, name)
|
||||
if strings.Contains(strings.ToLower(name), trans) {
|
||||
transName = name
|
||||
transId = id
|
||||
transMeta = transition.(map[string]interface{})
|
||||
}
|
||||
}
|
||||
if transId == "" {
|
||||
err := fmt.Errorf("Invalid Transition '%s', Available: %s", trans, strings.Join(found, ", "))
|
||||
log.Error("%s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
handlePost := func(json string) error {
|
||||
log.Debug("POST: %s", json)
|
||||
// os.Exit(0)
|
||||
uri = fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", c.endpoint, issue)
|
||||
resp, err := c.post(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == 204 {
|
||||
c.Browse(issue)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From POST")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
} else {
|
||||
issueData = data.(map[string]interface{})
|
||||
}
|
||||
issueData["meta"] = transMeta
|
||||
issueData["overrides"] = c.opts
|
||||
issueData["transition"] = map[string]interface{}{
|
||||
"name": transName,
|
||||
"id": transId,
|
||||
}
|
||||
|
||||
return c.editTemplate(
|
||||
c.getTemplate("transition"),
|
||||
fmt.Sprintf("%s-trans-%s-", issue, trans),
|
||||
issueData,
|
||||
handlePost,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdComment(issue string) error {
|
||||
log.Debug("comment called")
|
||||
|
||||
handlePost := func(json string) error {
|
||||
log.Debug("JSON: %s", json)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/comment", c.endpoint, issue)
|
||||
resp, err := c.post(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 201 {
|
||||
c.Browse(issue)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
return nil
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From POST")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if comment, ok := c.opts["comment"]; ok && comment != "" {
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"body": comment,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handlePost(json)
|
||||
} else {
|
||||
return c.editTemplate(
|
||||
c.getTemplate("comment"),
|
||||
fmt.Sprintf("%s-create-", issue),
|
||||
map[string]interface{}{},
|
||||
handlePost,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdAssign(issue string, user string) error {
|
||||
log.Debug("assign called")
|
||||
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"name": user,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/assignee", c.endpoint, issue)
|
||||
resp, err := c.put(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == 204 {
|
||||
c.Browse(issue)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
} 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) CmdExportTemplates() error {
|
||||
dir := c.opts["directory"]
|
||||
if err := mkdir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
continue
|
||||
}
|
||||
if fh, err := os.OpenFile(templateFile, os.O_WRONLY|os.O_CREATE, 0644); err != nil {
|
||||
log.Error("Failed to open %s for writing: %s", templateFile, err)
|
||||
return err
|
||||
} else {
|
||||
defer fh.Close()
|
||||
log.Notice("Creating %s", templateFile)
|
||||
fh.Write([]byte(template))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package cli
|
||||
|
||||
var all_templates = map[string]string{
|
||||
"debug": default_debug_template,
|
||||
"fields": default_debug_template,
|
||||
"editmeta": default_debug_template,
|
||||
"transmeta": default_debug_template,
|
||||
"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,
|
||||
"issuetypes": default_issuetypes_template,
|
||||
"create": default_create_template,
|
||||
"comment": default_comment_template,
|
||||
"transition": default_transition_template,
|
||||
}
|
||||
|
||||
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 }}
|
||||
summary: {{ .fields.summary }}
|
||||
project: {{ .fields.project.key }}
|
||||
components: {{ range .fields.components }}{{ .name }} {{end}}
|
||||
issuetype: {{ .fields.issuetype.name }}
|
||||
assignee: {{ if .fields.assignee }}{{ .fields.assignee.name }}{{end}}
|
||||
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}}
|
||||
priority: {{ .fields.priority.name }}
|
||||
description: |
|
||||
{{ or .fields.description "" | indent 2 }}
|
||||
|
||||
comments:
|
||||
{{ range .fields.comment.comments }} - | # {{.author.name}} at {{.created}}
|
||||
{{ or .body "" | indent 4}}
|
||||
{{end}}
|
||||
`
|
||||
const default_edit_template = `update:
|
||||
comment:
|
||||
- add:
|
||||
body: |
|
||||
{{ or .overrides.comment "" | indent 10 }}
|
||||
fields:
|
||||
summary: {{ or .overrides.summary .fields.summary }}
|
||||
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{if .overrides.components }}{{ range (split "," .overrides.components)}}
|
||||
- name: {{.}}{{end}}{{else}}{{ range .fields.components }}
|
||||
- name: {{ .name }}{{end}}{{end}}
|
||||
assignee:
|
||||
name: {{ if .overrides.assignee }}{{.overrides.assignee}}{{else}}{{if .fields.assignee }}{{ .fields.assignee.name }}{{end}}{{end}}
|
||||
reporter:
|
||||
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}}
|
||||
- name: {{ .overrides.watcher}}{{end}}
|
||||
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
|
||||
name: {{ or .overrides.priority .fields.priority.name }}
|
||||
description: |
|
||||
{{ or .overrides.description (or .fields.description "") | indent 4 }}
|
||||
`
|
||||
const default_transitions_template = `{{ range .transitions }}{{.id }}: {{.name}}
|
||||
{{end}}`
|
||||
|
||||
const default_issuetypes_template = `{{ range .projects }}{{ range .issuetypes }}{{color "+bh"}}{{.name | append ":" | printf "%-13s" }}{{color "reset"}} {{.description}}
|
||||
{{end}}{{end}}`
|
||||
|
||||
const default_create_template = `fields:
|
||||
project:
|
||||
key: {{ .overrides.project }}
|
||||
issuetype:
|
||||
name: {{ .overrides.issuetype }}
|
||||
summary: {{ or .overrides.summary "" }}
|
||||
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
|
||||
name: {{ or .overrides.priority "unassigned" }}
|
||||
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}}
|
||||
- name: {{ . }}{{end}}
|
||||
description: |
|
||||
{{ or .overrides.description "" | indent 4 }}
|
||||
assignee:
|
||||
name: {{ or .overrides.assignee "" }}
|
||||
reporter:
|
||||
name: {{ or .overrides.reporter .overrides.user }}
|
||||
# watchers
|
||||
customfield_10110: {{ range split "," (or .overrides.watchers "")}}
|
||||
- name: {{.}}{{end}}
|
||||
- name:
|
||||
`
|
||||
|
||||
const default_comment_template = `body: |
|
||||
{{ or .overrides.comment "" | indent 2 }}
|
||||
`
|
||||
|
||||
const default_transition_template = `update:
|
||||
comment:
|
||||
- add:
|
||||
body: |
|
||||
{{ or .overrides.comment "" | indent 10 }}
|
||||
fields:{{if .meta.fields.assignee}}
|
||||
assignee:
|
||||
name: {{if .overrides.assignee}}{{.overrides.assignee}}{{else}}{{if .fields.assignee}}{{.fields.assignee.name}}{{end}}{{end}}{{end}}{{if .meta.fields.components}}
|
||||
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{if .overrides.components }}{{ range (split "," .overrides.components)}}
|
||||
- name: {{.}}{{end}}{{else}}{{ range .fields.components }}
|
||||
- name: {{ .name }}{{end}}{{end}}{{end}}{{if .meta.fields.description}}
|
||||
description: {{or .overrides.description .fields.description }}{{end}}{{if .meta.fields.fixVersions}}{{if .meta.fields.fixVersions.allowedValues}}
|
||||
fixVersions: # Values: {{ range .meta.fields.fixVersions.allowedValues }}{{.name}}, {{end}}{{if .overrides.fixVersions}}{{ range (split "," .overrides.fixVersions)}}
|
||||
- name: {{.}}{{end}}{{else}}{{range .fields.fixVersions}}
|
||||
- name: {{.}}{{end}}{{end}}{{end}}{{end}}{{if .meta.fields.issuetype}}
|
||||
issuetype: # Values: {{ range .meta.fields.issuetype.allowedValues }}{{.name}}, {{end}}
|
||||
name: {{if .overrides.issuetype}}{{.overrides.issuetype}}{{else}}{{if .fields.issuetype}}{{.fields.issuetype.name}}{{end}}{{end}}{{end}}{{if .meta.fields.labels}}
|
||||
labels: {{range .fields.labels}}
|
||||
- {{.}}{{end}}{{if .overrides.labels}}{{range (split "," .overrides.labels)}}
|
||||
- {{.}}{{end}}{{end}}{{end}}{{if .meta.fields.priority}}
|
||||
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
|
||||
name: {{ or .overrides.priority "unassigned" }}{{end}}{{if .meta.fields.reporter}}
|
||||
reporter:
|
||||
name: {{if .overrides.reporter}}{{.overrides.reporter}}{{else}}{{if .fields.reporter}}{{.fields.reporter.name}}{{end}}{{end}}{{end}}{{if .meta.fields.resolution}}
|
||||
resolution: # Values: {{ range .meta.fields.resolution.allowedValues }}{{.name}}, {{end}}
|
||||
name: {{if .overrides.resolution}}{{.overrides.resolution}}{{else if .fields.resolution}}{{.fields.resolution.name}}{{else}}Fixed{{end}}{{end}}{{if .meta.fields.summary}}
|
||||
summary: {{or .overrides.summary .fields.summary}}{{end}}{{if .meta.fields.versions.allowedValues}}
|
||||
versions: # Values: {{ range .meta.fields.versions.allowedValues }}{{.name}}, {{end}}{{if .overrides.versions}}{{ range (split "," .overrides.versions)}}
|
||||
- name: {{.}}{{end}}{{else}}{{range .fields.versions}}
|
||||
- name: {{.}}{{end}}{{end}}{{end}}
|
||||
transition:
|
||||
id: {{ .transition.id }}
|
||||
name: {{ .transition.name }}
|
||||
`
|
||||
@@ -0,0 +1,292 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mgutz/ansi"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
func FindParentPaths(fileName string) []string {
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
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 == "/" {
|
||||
dir = fmt.Sprintf("/%s", part)
|
||||
} else {
|
||||
dir = fmt.Sprintf("%s/%s", dir, part)
|
||||
}
|
||||
file := fmt.Sprintf("%s/%s", dir, fileName)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
paths = append(paths, file)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func FindClosestParentPath(fileName string) (string, error) {
|
||||
paths := FindParentPaths(fileName)
|
||||
if len(paths) > 0 {
|
||||
return paths[len(paths)-1], nil
|
||||
}
|
||||
return "", errors.New(fmt.Sprintf("%s not found in parent directory hierarchy", fileName))
|
||||
}
|
||||
|
||||
func readFile(file string) string {
|
||||
var bytes []byte
|
||||
var err error
|
||||
if bytes, err = ioutil.ReadFile(file); err != nil {
|
||||
log.Error("Failed to read file %s: %s", file, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
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 {
|
||||
out = os.Stdout
|
||||
}
|
||||
|
||||
funcs := map[string]interface{}{
|
||||
"toJson": func(content interface{}) (string, error) {
|
||||
if bytes, err := json.MarshalIndent(content, "", " "); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return string(bytes), nil
|
||||
}
|
||||
},
|
||||
"append": func(more string, content interface{}) (string, error) {
|
||||
switch value := content.(type) {
|
||||
case string:
|
||||
return string(append([]byte(content.(string)), []byte(more)...)), nil
|
||||
case []byte:
|
||||
return string(append(content.([]byte), []byte(more)...)), nil
|
||||
default:
|
||||
return "", errors.New(fmt.Sprintf("Unknown type: %s", value))
|
||||
}
|
||||
},
|
||||
"indent": func(spaces int, content string) string {
|
||||
indent := make([]byte, spaces+1, spaces+1)
|
||||
indent[0] = '\n'
|
||||
for i := 1; i < spaces+1; i += 1 {
|
||||
indent[i] = ' '
|
||||
}
|
||||
return strings.Replace(content, "\n", string(indent), -1)
|
||||
},
|
||||
"color": func(color string) string {
|
||||
return ansi.ColorCode(color)
|
||||
},
|
||||
"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)
|
||||
return err
|
||||
} else {
|
||||
if err := tmpl.Execute(out, data); err != nil {
|
||||
log.Error("Failed to execute template: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func responseToJson(resp *http.Response, err error) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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{} {
|
||||
content, err := ioutil.ReadAll(io)
|
||||
var data interface{}
|
||||
err = json.Unmarshal(content, &data)
|
||||
if err != nil {
|
||||
log.Error("JSON Parse Error: %s from %s", err, content)
|
||||
}
|
||||
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()
|
||||
if err != nil {
|
||||
log.Error("Failed to open %s: %s", file, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
enc := json.NewEncoder(fh)
|
||||
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(strings.TrimRight(text, "\n"))
|
||||
if ans == "" {
|
||||
return yes
|
||||
}
|
||||
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 if fixed != nil {
|
||||
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 if fixed != nil {
|
||||
d[k] = fixed
|
||||
}
|
||||
}
|
||||
return d, nil
|
||||
case []interface{}:
|
||||
for i, val := range d {
|
||||
if fixed, err := yamlFixup(val); err != nil {
|
||||
return nil, err
|
||||
} else if fixed != nil {
|
||||
d[i] = fixed
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
case string:
|
||||
if d == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return d, nil
|
||||
default:
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
|
||||
func mkdir(dir string) error {
|
||||
if stat, err := os.Stat(dir); err != nil && !os.IsNotExist(err) {
|
||||
log.Error("Failed to stat %s: %s", dir, err)
|
||||
return err
|
||||
} else if err == nil && !stat.IsDir() {
|
||||
err := fmt.Errorf("%s exists and is not a directory!", dir)
|
||||
log.Error("%s", err)
|
||||
return err
|
||||
} else {
|
||||
// dir does not exist, so try to create it
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
log.Error("Failed to mkdir -p %s: %s", dir, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/Netflix-Skunkworks/go-jira/jira/cli"
|
||||
"github.com/docopt/docopt-go"
|
||||
"github.com/op/go-logging"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var log = logging.MustGetLogger("jira")
|
||||
var format = "%{color}%{time:2006-01-02T15:04:05.000Z07:00} %{level:-5s} [%{shortfile}]%{color:reset} %{message}"
|
||||
|
||||
func main() {
|
||||
user := os.Getenv("USER")
|
||||
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]) [-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]...
|
||||
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] [-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
|
||||
jira [-v ...] [-u USER] [-e URI] [-t FILE] fields
|
||||
jira [-v ...] [-u USER] [-e URI] [-t FILE] issuelinktypes
|
||||
jira [-v ...] [-u USER] [-e URI] [-b][-t FILE] transmeta ISSUE
|
||||
jira [-v ...] [-u USER] [-e URI] [-b] [-t FILE] editmeta 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] [-b] [-t FILE] transitions ISSUE
|
||||
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
|
||||
|
||||
General Options:
|
||||
-e --endpoint=URI URI to use for jira
|
||||
-h --help Show this usage
|
||||
-t --template=FILE Template file to use for output/editing
|
||||
-u --user=USER Username to use for authenticaion (default: %s)
|
||||
-v --verbose Increase output logging
|
||||
--version Show this version
|
||||
|
||||
Command Options:
|
||||
-a --assignee=USER Username assigned the issue
|
||||
-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 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.7", false, false)
|
||||
if err != nil {
|
||||
log.Error("Failed to parse options: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||
logging.SetBackend(
|
||||
logging.NewBackendFormatter(
|
||||
logBackend,
|
||||
logging.MustStringFormatter(format),
|
||||
),
|
||||
)
|
||||
logging.SetLevel(logging.NOTICE, "")
|
||||
if verbose, ok := args["--verbose"]; ok {
|
||||
if verbose.(int) > 1 {
|
||||
logging.SetLevel(logging.DEBUG, "")
|
||||
} else if verbose.(int) > 0 {
|
||||
logging.SetLevel(logging.INFO, "")
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Args: %v", args)
|
||||
|
||||
populateEnv(args)
|
||||
|
||||
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:]
|
||||
if opt == "override" {
|
||||
for _, v := range val.([]string) {
|
||||
if strings.Contains(v, "=") {
|
||||
kv := strings.SplitN(v, "=", 2)
|
||||
opts[kv[0]] = kv[1]
|
||||
} else {
|
||||
log.Error("Malformed override, expected KEY=VALUE, got %s", v)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
opts[opt] = v
|
||||
case int:
|
||||
opts[opt] = fmt.Sprintf("%d", v)
|
||||
case bool:
|
||||
opts[opt] = fmt.Sprintf("%t", 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["user"]; !ok {
|
||||
opts["user"] = user
|
||||
}
|
||||
if _, ok := opts["queryfields"]; !ok {
|
||||
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)
|
||||
|
||||
validCommand := func(cmd string) bool {
|
||||
if val, ok := args[cmd]; ok && val.(bool) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
validOpt := func(opt string, dflt interface{}) interface{} {
|
||||
if val, ok := opts[opt]; ok {
|
||||
return val
|
||||
}
|
||||
if dflt == nil {
|
||||
log.Error("Missing required option --%s or \"%s\" property override in the config file", opt, opt)
|
||||
os.Exit(1)
|
||||
}
|
||||
return dflt
|
||||
}
|
||||
|
||||
setEditing := func(dflt bool) {
|
||||
if dflt {
|
||||
if val, ok := opts["noedit"]; ok && val == "true" {
|
||||
opts["edit"] = "false"
|
||||
} else {
|
||||
opts["edit"] = "true"
|
||||
}
|
||||
} else {
|
||||
if val, ok := opts["edit"]; ok && val != "true" {
|
||||
opts["edit"] = "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if validCommand("login") {
|
||||
err = c.CmdLogin()
|
||||
} else if validCommand("fields") {
|
||||
err = c.CmdFields()
|
||||
} else if validCommand("ls") || validCommand("list") {
|
||||
err = c.CmdList()
|
||||
} else if validCommand("edit") {
|
||||
setEditing(true)
|
||||
err = c.CmdEdit(args["ISSUE"].(string))
|
||||
} else if validCommand("editmeta") {
|
||||
err = c.CmdEditMeta(args["ISSUE"].(string))
|
||||
} else if validCommand("transmeta") {
|
||||
err = c.CmdTransitionMeta(args["ISSUE"].(string))
|
||||
} else if validCommand("issuelinktypes") {
|
||||
err = c.CmdIssueLinkTypes()
|
||||
} else if validCommand("issuetypes") {
|
||||
err = c.CmdIssueTypes(validOpt("project", nil).(string))
|
||||
} else if validCommand("createmeta") {
|
||||
err = c.CmdCreateMeta(
|
||||
validOpt("project", nil).(string),
|
||||
validOpt("issuetype", "Bug").(string),
|
||||
)
|
||||
} else if validCommand("create") {
|
||||
setEditing(true)
|
||||
err = c.CmdCreate(
|
||||
validOpt("project", nil).(string),
|
||||
validOpt("issuetype", "Bug").(string),
|
||||
)
|
||||
} else if validCommand("transitions") {
|
||||
err = c.CmdTransitions(args["ISSUE"].(string))
|
||||
} else if validCommand("blocks") {
|
||||
err = c.CmdBlocks(
|
||||
args["BLOCKER"].(string),
|
||||
args["ISSUE"].(string),
|
||||
)
|
||||
} else if validCommand("dups") {
|
||||
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),
|
||||
validOpt("watcher", user).(string),
|
||||
)
|
||||
} else if validCommand("trans") || validCommand("transition") {
|
||||
setEditing(true)
|
||||
err = c.CmdTransition(
|
||||
args["ISSUE"].(string),
|
||||
args["TRANSITION"].(string),
|
||||
)
|
||||
} else if validCommand("close") {
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args["ISSUE"].(string), "close")
|
||||
} else if validCommand("ack") {
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args["ISSUE"].(string), "acknowledge")
|
||||
} else if validCommand("reopen") {
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args["ISSUE"].(string), "reopen")
|
||||
} else if validCommand("resolve") {
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args["ISSUE"].(string), "resolve")
|
||||
} else if validCommand("start") {
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args["ISSUE"].(string), "start")
|
||||
} else if validCommand("stop") {
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args["ISSUE"].(string), "stop")
|
||||
} else if validCommand("comment") {
|
||||
setEditing(true)
|
||||
err = c.CmdComment(args["ISSUE"].(string))
|
||||
} else if validCommand("take") {
|
||||
err = c.CmdAssign(args["ISSUE"].(string), opts["user"])
|
||||
} else if validCommand("browse") || validCommand("b") {
|
||||
opts["browse"] = "true"
|
||||
err = c.Browse(args["ISSUE"].(string))
|
||||
} else if validCommand("export-templates") {
|
||||
err = c.CmdExportTemplates()
|
||||
} else if validCommand("assign") || validCommand("give") {
|
||||
err = c.CmdAssign(
|
||||
args["ISSUE"].(string),
|
||||
args["ASSIGNEE"].(string),
|
||||
)
|
||||
} else if val, ok := args["ISSUE"]; ok {
|
||||
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 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 {
|
||||
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