mirror of
https://github.com/Threnklyn/jira.git
synced 2026-05-19 20:53:27 +02:00
Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e5cc9821e | |||
| 9e90376816 | |||
| 20b32c2ed6 | |||
| ac170e9ab1 | |||
| d8bce08d3a | |||
| 382bf4faeb | |||
| 595a5212b4 | |||
| f595801202 | |||
| 404caf6400 | |||
| f7eb04e36d | |||
| b0d4f7273d | |||
| a927181db1 | |||
| b5417ef585 | |||
| c5af781c41 | |||
| f2c4df9b3e | |||
| 1dde7e06e6 | |||
| 7bc1897792 | |||
| 37aab3580b | |||
| ff56136937 | |||
| 42990d8ca0 | |||
| 8e662462da | |||
| ad7bb2b724 | |||
| a8cce44178 | |||
| 35955a7a93 | |||
| f349e25bb9 | |||
| a92a93b282 | |||
| 8645ef11f1 | |||
| e042a3e62a | |||
| a738d1515e | |||
| d4f15ae5c6 | |||
| bc70b43868 | |||
| e24b431b7a | |||
| 101bc1da68 | |||
| 63e035c5c1 | |||
| 40bafc9b66 | |||
| 5d863ffed4 | |||
| 577394b0bd | |||
| c1a7e1bbdb | |||
| f904f3c089 | |||
| e35e518368 | |||
| 159d142f37 | |||
| df84d47552 | |||
| fa4ce5647d | |||
| 713d300a57 | |||
| c8ae7fc685 | |||
| 80322b648e | |||
| f2076a0977 | |||
| 886adb5db2 | |||
| 3bdbdbdaff | |||
| 544b923fab | |||
| 13a69e6f44 | |||
| fae9f94817 | |||
| 7d90672736 | |||
| 20faa959aa | |||
| aaff47d606 | |||
| 7bfb2946d4 | |||
| 9884281079 | |||
| 03fce96eb5 | |||
| 8f9e6f7d85 | |||
| 65c0240d34 | |||
| ccec749a0b | |||
| 6a9901f171 | |||
| 868764ac86 | |||
| f1e7514a00 | |||
| 92d10c3498 | |||
| a9c6b865b6 | |||
| 9cc55a13c1 | |||
| b116764d3e | |||
| a4aa52fb58 | |||
| 582f37866f | |||
| ab7f194647 | |||
| e8ba1d6053 | |||
| 4dc23487ba | |||
| 8f44cc5de7 | |||
| a989198630 | |||
| 7f85842df3 | |||
| f1921454d6 | |||
| d7f6638f2a | |||
| ae285b5a55 | |||
| 5d8dc9f9d8 | |||
| ade9f5f1f5 | |||
| a990fdf7bb | |||
| 0f217cefb5 | |||
| 38b3e90198 | |||
| 80b6f5a198 | |||
| 794654dcd7 | |||
| a36bf387fa | |||
| b134e7c6f7 | |||
| b8889c656d | |||
| 8949c12354 |
@@ -1,5 +1,96 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0 - 2016-01-29
|
||||
|
||||
* Fixes [#32](https://github.com/Netflix-Skunkworks/go-jira/issues/32) - make path to cookieFile if it's not present [Mike Pountney] [[6644579](https://github.com/Netflix-Skunkworks/go-jira/commit/6644579)]
|
||||
* Add component/components support: add and list for now. [Mike Pountney] [[d7b3226](https://github.com/Netflix-Skunkworks/go-jira/commit/d7b3226)]
|
||||
* Tweak the CmdWatch contract and add watcher remove support [Mike Pountney] [[383847a](https://github.com/Netflix-Skunkworks/go-jira/commit/383847a)]
|
||||
* Amend vote/unvote to be vote/vote --down [Mike Pountney] [[797edef](https://github.com/Netflix-Skunkworks/go-jira/commit/797edef)]
|
||||
* Add 'vote' and 'unvote' [Mike Pountney] [[c95e66e](https://github.com/Netflix-Skunkworks/go-jira/commit/c95e66e)]
|
||||
|
||||
## 0.0.20 - 2016-01-21
|
||||
|
||||
* [issue [#28](https://github.com/Netflix-Skunkworks/go-jira/issues/28)] check to make sure we got back issuetypes for create metadata [Cory Bennett] [[ee0e780](https://github.com/Netflix-Skunkworks/go-jira/commit/ee0e780)]
|
||||
* Add insecure option for TLS endpoints [Brian Lalor] [[6a88bb9](https://github.com/Netflix-Skunkworks/go-jira/commit/6a88bb9)]
|
||||
* Correct naming of parameter: set/add/remove are actions. [Mike Pountney] [[303784f](https://github.com/Netflix-Skunkworks/go-jira/commit/303784f)]
|
||||
* Tweak CmdLabels args so that magic happens with CLI [Mike Pountney] [[40a7c65](https://github.com/Netflix-Skunkworks/go-jira/commit/40a7c65)]
|
||||
* Expose ViewTicket as per FindIssues [Mike Pountney] [[8977f3d](https://github.com/Netflix-Skunkworks/go-jira/commit/8977f3d)]
|
||||
* Add exposed versions of getTemplate and runTemplate [Mike Pountney] [[da6cbd5](https://github.com/Netflix-Skunkworks/go-jira/commit/da6cbd5)]
|
||||
* Add 'labels' command to set/add/remove labels [Mike Pountney] [[230b52d](https://github.com/Netflix-Skunkworks/go-jira/commit/230b52d)]
|
||||
* Add a 'join' func to the template engine [Mike Pountney] [[a7820fe](https://github.com/Netflix-Skunkworks/go-jira/commit/a7820fe)]
|
||||
* make "jira" golang package, move code from jira/cli to root, move jira/main.go to main/main.go [Cory Bennett] [[7268b9e](https://github.com/Netflix-Skunkworks/go-jira/commit/7268b9e)]
|
||||
|
||||
## 0.0.19 - 2015-12-09
|
||||
|
||||
* fix jira trans TRANS ISSUE (case sensitivity issue), also go fmt [Cory Bennett] [[3c30f3b](https://github.com/Netflix-Skunkworks/go-jira/commit/3c30f3b)]
|
||||
|
||||
## 0.0.18 - 2015-12-03
|
||||
|
||||
* need to default "quiet" to false [Cory Bennett] [[4f4a89b](https://github.com/Netflix-Skunkworks/go-jira/commit/4f4a89b)]
|
||||
|
||||
## 0.0.17 - 2015-12-03
|
||||
|
||||
* add --quiet command to not print the OK .. add --saveFile option to print the issue/link to a file on create command [Cory Bennett] [[c9ac162](https://github.com/Netflix-Skunkworks/go-jira/commit/c9ac162)]
|
||||
* fix overrides [Cory Bennett] [[eaddfe6](https://github.com/Netflix-Skunkworks/go-jira/commit/eaddfe6)]
|
||||
* add abstract request wrapper to allow you to access/process random apis supported by Jira but not yet supported by go-jira [Cory Bennett] [[90ef56a](https://github.com/Netflix-Skunkworks/go-jira/commit/90ef56a)]
|
||||
|
||||
## 0.0.16 - 2015-11-23
|
||||
|
||||
* jira edit should not require one arguemnt (allow for --query) [Cory Bennett] [[a1eb4a1](https://github.com/Netflix-Skunkworks/go-jira/commit/a1eb4a1)]
|
||||
|
||||
## 0.0.15 - 2015-11-23
|
||||
|
||||
* [[#17](https://github.com/Netflix-Skunkworks/go-jira/issues/17)] print usage on missing arguments [Cory Bennett] [[fd2a2fe](https://github.com/Netflix-Skunkworks/go-jira/commit/fd2a2fe)]
|
||||
|
||||
## 0.0.14 - 2015-11-17
|
||||
|
||||
* s/enpoint/endpoint/g [Oliver Schrenk] [[c5d251d](https://github.com/Netflix-Skunkworks/go-jira/commit/c5d251d)]
|
||||
* Implement dateFormat template command [Mike Pountney] [[68d3bae](https://github.com/Netflix-Skunkworks/go-jira/commit/68d3bae)]
|
||||
* Add 'updated' field to default queryfields. [Mike Pountney] [[91e2475](https://github.com/Netflix-Skunkworks/go-jira/commit/91e2475)]
|
||||
* Fix export-templates option (typo) [Mike Pountney] [[4d7fdb8](https://github.com/Netflix-Skunkworks/go-jira/commit/4d7fdb8)]
|
||||
* when yaml element resolves to "\n" strip it out so we dont post it to jira [Cory Bennett] [[47ced2f](https://github.com/Netflix-Skunkworks/go-jira/commit/47ced2f)]
|
||||
* print PUT/POST data when using --dryrun to help debug [Cory Bennett] [[618f245](https://github.com/Netflix-Skunkworks/go-jira/commit/618f245)]
|
||||
|
||||
## 0.0.13 - 2015-09-19
|
||||
|
||||
* replace dead/deprecated code.google.com/p/gopass with golang.org/x/crypto/ssh/terminal for reading password from stdin [Cory Bennett] [[909eb06](https://github.com/Netflix-Skunkworks/go-jira/commit/909eb06)]
|
||||
|
||||
## 0.0.12 - 2015-09-18
|
||||
|
||||
* fix exception from "jira create" [Cory Bennett] [[9348a4b](https://github.com/Netflix-Skunkworks/go-jira/commit/9348a4b)]
|
||||
* add some debug messages to help diagnose login failures [Cory Bennett] [[1c08a7d](https://github.com/Netflix-Skunkworks/go-jira/commit/1c08a7d)]
|
||||
|
||||
## 0.0.11 - 2015-09-16
|
||||
|
||||
* add --version [Cory Bennett] [[8385ee2](https://github.com/Netflix-Skunkworks/go-jira/commit/8385ee2)]
|
||||
* fix command line parser broken in 0.0.10 [Cory Bennett] [[15ae929](https://github.com/Netflix-Skunkworks/go-jira/commit/15ae929)]
|
||||
|
||||
## 0.0.10 - 2015-09-15
|
||||
|
||||
* allow for command aliasing in conjunction with executable config files. Issue #5 [Cory Bennett] [[23590d4](https://github.com/Netflix-Skunkworks/go-jira/commit/23590d4)]
|
||||
* update usage [Cory Bennett] [[ef7a57e](https://github.com/Netflix-Skunkworks/go-jira/commit/ef7a57e)]
|
||||
|
||||
## 0.0.9 - 2015-09-15
|
||||
|
||||
* use forked yaml.v2 so as to not lose line terminations present in jira fields [Cory Bennett] [[f84e77f](https://github.com/Netflix-Skunkworks/go-jira/commit/f84e77f)]
|
||||
* adding a |~ literal yaml syntax to just chomp a single newline (again to preserve existing formatting in jira fields) [Cory Bennett] [[f84e77f](https://github.com/Netflix-Skunkworks/go-jira/commit/f84e77f)]
|
||||
* for indent/comment allow for unicode line termination characters that yaml will use for parsing [Cory Bennett] [[f84e77f](https://github.com/Netflix-Skunkworks/go-jira/commit/f84e77f)]
|
||||
* fix "edit" default option, change how defaults are dealt with for filters [Cory Bennett] [[4265913](https://github.com/Netflix-Skunkworks/go-jira/commit/4265913)]
|
||||
* for edit template add issue id as comment, also add "comments" as comment so you can review the comment details while editing [Cory Bennett] [[968a9df](https://github.com/Netflix-Skunkworks/go-jira/commit/968a9df)]
|
||||
* add "comment" template filter to comment out multiline statements [Cory Bennett] [[d664868](https://github.com/Netflix-Skunkworks/go-jira/commit/d664868)]
|
||||
* add getOpt wrappers to get options with defaults [Cory Bennett] [[c0070cf](https://github.com/Netflix-Skunkworks/go-jira/commit/c0070cf)]
|
||||
* make --dryrun work [Cory Bennett] [[d229ac1](https://github.com/Netflix-Skunkworks/go-jira/commit/d229ac1)]
|
||||
* refactor config/option loading so command options override settings in config files [Cory Bennett] [[d229ac1](https://github.com/Netflix-Skunkworks/go-jira/commit/d229ac1)]
|
||||
* allow query options to be used on the "edit" command to iterate editing [Cory Bennett] [[d229ac1](https://github.com/Netflix-Skunkworks/go-jira/commit/d229ac1)]
|
||||
* remove duplication for defaults [Cory Bennett] [[f8c8ddf](https://github.com/Netflix-Skunkworks/go-jira/commit/f8c8ddf)]
|
||||
* use optigo for option parsing, drop docopt [Cory Bennett] [[7bbd571](https://github.com/Netflix-Skunkworks/go-jira/commit/7bbd571)]
|
||||
* allow "abort: true" to be set while editing to cancel the edit operation [Cory Bennett] [[ea67a77](https://github.com/Netflix-Skunkworks/go-jira/commit/ea67a77)]
|
||||
* if no changes are made on edit templates then abort edit [Cory Bennett] [[e69b65c](https://github.com/Netflix-Skunkworks/go-jira/commit/e69b65c)]
|
||||
|
||||
## 0.0.8 - 2015-07-31
|
||||
|
||||
* Add --max_results option for 'ls' [Mike Pountney] [[e06ff0c](https://github.com/Netflix-Skunkworks/go-jira/commit/e06ff0c)]
|
||||
|
||||
## 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)]
|
||||
|
||||
@@ -14,46 +14,74 @@ PLATFORMS= \
|
||||
$(NULL)
|
||||
|
||||
DIST=$(shell pwd)/dist
|
||||
|
||||
export GOPATH=$(shell pwd)
|
||||
|
||||
build:
|
||||
cd src/github.com/Netflix-Skunkworks/go-jira/jira; \
|
||||
go get -v
|
||||
GOBIN ?= $(shell pwd)
|
||||
NAME=jira
|
||||
|
||||
BIN ?= $(GOBIN)/$(NAME)
|
||||
|
||||
CURVER ?= $(shell [ -d .git ] && git describe --abbrev=0 --tags || grep ^\#\# CHANGELOG.md | awk '{print $$2; exit}')
|
||||
LDFLAGS:=-X jira.VERSION=$(patsubst v%,%,$(CURVER)) -w
|
||||
|
||||
# use make DEBUG=1 and you can get a debuggable golang binary
|
||||
# see https://github.com/mailgun/godebug
|
||||
ifneq ($(DEBUG),)
|
||||
GOBUILD=go get -v github.com/mailgun/godebug && ./bin/godebug build
|
||||
else
|
||||
GOBUILD=go build -v -ldflags "$(LDFLAGS) -s"
|
||||
endif
|
||||
|
||||
build: src/github.com/Netflix-Skunkworks/go-jira
|
||||
$(GOBUILD) -o $(BIN) main/main.go
|
||||
|
||||
src/%:
|
||||
mkdir -p $(@D)
|
||||
test -L $@ || ln -sf ../../.. $@
|
||||
go get -v $* $*/main
|
||||
|
||||
cross-setup:
|
||||
for p in $(PLATFORMS); do \
|
||||
echo "Building for $$p"; \
|
||||
cd $(GOROOT)/src && sudo GOOS=$${p/-*/} GOARCH=$${p/*-/} bash ./make.bash --no-clean; \
|
||||
cd $(GOROOT)/src && sudo GOROOT_BOOTSTRAP=$(GOROOT) 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
|
||||
${MAKE} build GOOS=$${p/-*/} GOARCH=$${p/*-/} BIN=$(DIST)/$(NAME)-$$p; \
|
||||
done
|
||||
|
||||
fmt:
|
||||
gofmt -s -w jira
|
||||
gofmt -s -w main/*.go *.go
|
||||
|
||||
CURVER := $(shell grep '\#\#' CHANGELOG.md | awk '{print $$2; exit}')
|
||||
NEWVER := $(shell awk -F'"' '/docopt.Parse/{print $$2}' jira/main.go)
|
||||
install:
|
||||
${MAKE} GOBIN=~/bin build
|
||||
|
||||
NEWVER ?= $(shell echo $(CURVER) | awk -F. '{print $$1"."$$2"."$$3+1}')
|
||||
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"
|
||||
@git log --pretty=format:"* %s [%cn] [%h]" --no-merges ^$(CURVER) HEAD main/*.go *.go | grep -vE 'gofmt|go fmt'
|
||||
|
||||
update-changelog:
|
||||
update-changelog:
|
||||
@echo "# Changelog" > CHANGELOG.md.new; \
|
||||
echo >> CHANGELOG.md.new; \
|
||||
echo "## $(NEWVER) - $(TODAY)" >> CHANGELOG.md.new; \
|
||||
echo >> CHANGELOG.md.new; \
|
||||
$(MAKE) changes | \
|
||||
$(MAKE) --no-print-directory --silent 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
|
||||
tail -n +2 CHANGELOG.md >> CHANGELOG.md.new; \
|
||||
mv CHANGELOG.md.new CHANGELOG.md; \
|
||||
git commit -m "Updated Changelog" CHANGELOG.md; \
|
||||
git tag v$(NEWVER)
|
||||
|
||||
version:
|
||||
@echo $(patsubst v%,%,$(CURVER))
|
||||
|
||||
clean:
|
||||
rm -rf pkg dist bin src ./$(NAME)
|
||||
|
||||
@@ -171,56 +171,65 @@ hard-coded templates with `jira export-templates` which will write them to **~/.
|
||||
|
||||
```
|
||||
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] [--max_results MAX_RESULTS]
|
||||
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
|
||||
|
||||
jira (ls|list) <Query Options>
|
||||
jira view ISSUE
|
||||
jira edit [--noedit] <Edit Options> [ISSUE | <Query Options>]
|
||||
jira create [--noedit] [-p PROJECT] <Create Options>
|
||||
jira DUPLICATE dups ISSUE
|
||||
jira BLOCKER blocks ISSUE
|
||||
jira watch ISSUE [-w WATCHER]
|
||||
jira (trans|transition) TRANSITION ISSUE [--noedit] <Edit Options>
|
||||
jira ack ISSUE [--edit] <Edit Options>
|
||||
jira close ISSUE [--edit] <Edit Options>
|
||||
jira resolve ISSUE [--edit] <Edit Options>
|
||||
jira reopen ISSUE [--edit] <Edit Options>
|
||||
jira start ISSUE [--edit] <Edit Options>
|
||||
jira stop ISSUE [--edit] <Edit Options>
|
||||
jira comment ISSUE [--noedit] <Edit Options>
|
||||
jira take ISSUE
|
||||
jira (assign|give) ISSUE ASSIGNEE
|
||||
jira fields
|
||||
jira issuelinktypes
|
||||
jira transmeta ISSUE
|
||||
jira editmeta ISSUE
|
||||
jira issuetypes [-p PROJECT]
|
||||
jira createmeta [-p PROJECT] [-i ISSUETYPE]
|
||||
jira transitions ISSUE
|
||||
jira export-templates [-d DIR] [-t template]
|
||||
jira (b|browse) ISSUE
|
||||
jira login
|
||||
jira ISSUE
|
||||
|
||||
General Options:
|
||||
-b --browse Open your browser to the Jira issue
|
||||
-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)
|
||||
-u --user=USER Username to use for authenticaion (default: $USER)
|
||||
-v --verbose Increase output logging
|
||||
--version Show this version
|
||||
|
||||
Command Options:
|
||||
Query 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
|
||||
-f --queryfields=FIELDS Fields that are used in "list" template: (default: summary,created,updated,priority,status,reporter,assignee)
|
||||
-i --issuetype=ISSUETYPE The Issue Type
|
||||
-l --limit=VAL Maximum number of results to return in query (default: 500)
|
||||
-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)
|
||||
-s --sort=ORDER For list operations, sort issues (default: priority asc, created)
|
||||
-w --watcher=USER Watcher to add to issue (default: $USER)
|
||||
or Watcher to search for
|
||||
--max_results=VAL Maximum number of results to return in query (default: 500)
|
||||
|
||||
Edit Options:
|
||||
-m --comment=COMMENT Comment message for transition
|
||||
-o --override=KEY=VAL Set custom key/value pairs
|
||||
|
||||
Create Options:
|
||||
-i --issuetype=ISSUETYPE Jira Issue Type (default: Bug)
|
||||
-m --comment=COMMENT Comment message for transition
|
||||
-o --override=KEY=VAL Set custom key/value pairs
|
||||
|
||||
Command Options:
|
||||
-d --directory=DIR Directory to export templates to (default: $HOME/.jira.d/templates)
|
||||
```
|
||||
|
||||
+187
-15
@@ -1,47 +1,63 @@
|
||||
package cli
|
||||
package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/op/go-logging"
|
||||
"gopkg.in/yaml.v2"
|
||||
"gopkg.in/coryb/yaml.v2"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var log = logging.MustGetLogger("jira.cli")
|
||||
var (
|
||||
log = logging.MustGetLogger("jira")
|
||||
VERSION string
|
||||
)
|
||||
|
||||
type Cli struct {
|
||||
endpoint *url.URL
|
||||
opts map[string]string
|
||||
opts map[string]interface{}
|
||||
cookieFile string
|
||||
ua *http.Client
|
||||
}
|
||||
|
||||
func New(opts map[string]string) *Cli {
|
||||
func New(opts map[string]interface{}) *Cli {
|
||||
homedir := os.Getenv("HOME")
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
endpoint, _ := opts["endpoint"]
|
||||
endpoint, _ := opts["endpoint"].(string)
|
||||
url, _ := url.Parse(strings.TrimRight(endpoint, "/"))
|
||||
|
||||
if project, ok := opts["project"]; ok {
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{},
|
||||
}
|
||||
|
||||
if project, ok := opts["project"].(string); ok {
|
||||
opts["project"] = strings.ToUpper(project)
|
||||
}
|
||||
|
||||
if insecureSkipVerify, ok := opts["insecure"].(bool); ok {
|
||||
transport.TLSClientConfig.InsecureSkipVerify = insecureSkipVerify
|
||||
}
|
||||
|
||||
cli := &Cli{
|
||||
endpoint: url,
|
||||
opts: opts,
|
||||
cookieFile: fmt.Sprintf("%s/.jira.d/cookies.js", homedir),
|
||||
ua: &http.Client{Jar: cookieJar},
|
||||
ua: &http.Client{
|
||||
Jar: cookieJar,
|
||||
Transport: transport,
|
||||
},
|
||||
}
|
||||
|
||||
cli.ua.Jar.SetCookies(url, cli.loadCookies())
|
||||
@@ -72,6 +88,7 @@ func (c *Cli) saveCookies(cookies []*http.Cookie) {
|
||||
}
|
||||
jsonWrite(c.cookieFile, mergedCookies)
|
||||
} else {
|
||||
mkdir(path.Dir(c.cookieFile))
|
||||
jsonWrite(c.cookieFile, cookies)
|
||||
}
|
||||
}
|
||||
@@ -103,6 +120,24 @@ func (c *Cli) put(uri string, content string) (*http.Response, error) {
|
||||
return c.makeRequestWithContent("PUT", uri, content)
|
||||
}
|
||||
|
||||
func (c *Cli) delete(uri string) (*http.Response, error) {
|
||||
method := "DELETE"
|
||||
req, _ := http.NewRequest(method, uri, nil)
|
||||
log.Info("%s %s", req.Method, req.URL.String())
|
||||
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, nil)
|
||||
return c.makeRequest(req)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) makeRequestWithContent(method string, uri string, content string) (*http.Response, error) {
|
||||
buffer := bytes.NewBufferString(content)
|
||||
req, _ := http.NewRequest(method, uri, buffer)
|
||||
@@ -174,8 +209,12 @@ func (c *Cli) makeRequest(req *http.Request) (resp *http.Response, err error) {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Cli) GetTemplate(name string) string {
|
||||
return c.getTemplate(name)
|
||||
}
|
||||
|
||||
func (c *Cli) getTemplate(name string) string {
|
||||
if override, ok := c.opts["template"]; ok {
|
||||
if override, ok := c.opts["template"].(string); ok {
|
||||
if _, err := os.Stat(override); err == nil {
|
||||
return readFile(override)
|
||||
} else {
|
||||
@@ -203,6 +242,12 @@ func (c *Cli) getTemplate(name string) string {
|
||||
}
|
||||
}
|
||||
|
||||
type NoChangesFound struct{}
|
||||
|
||||
func (f NoChangesFound) Error() string {
|
||||
return "No changes found, aborting"
|
||||
}
|
||||
|
||||
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"))
|
||||
@@ -222,6 +267,9 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
log.Error("Failed to rename %s to %s: %s", fh.Name(), fmt.Sprintf("%s.yml", fh.Name()), err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
os.Remove(tmpFileName)
|
||||
}()
|
||||
|
||||
err = runTemplate(template, templateData, fh)
|
||||
if err != nil {
|
||||
@@ -230,7 +278,7 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
|
||||
fh.Close()
|
||||
|
||||
editor, ok := c.opts["editor"]
|
||||
editor, ok := c.opts["editor"].(string)
|
||||
if !ok {
|
||||
editor = os.Getenv("JIRA_EDITOR")
|
||||
if editor == "" {
|
||||
@@ -241,10 +289,13 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
}
|
||||
}
|
||||
|
||||
editing := true
|
||||
if val, ok := c.opts["edit"]; ok && val == "false" {
|
||||
editing = false
|
||||
}
|
||||
editing := c.getOptBool("edit", true)
|
||||
|
||||
tmpFileNameOrig := fmt.Sprintf("%s.orig", tmpFileName)
|
||||
copyFile(tmpFileName, tmpFileNameOrig)
|
||||
defer func() {
|
||||
os.Remove(tmpFileNameOrig)
|
||||
}()
|
||||
|
||||
for true {
|
||||
if editing {
|
||||
@@ -260,6 +311,12 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
diff := exec.Command("diff", "-q", tmpFileNameOrig, tmpFileName)
|
||||
// if err == nil then diff found no changes
|
||||
if err := diff.Run(); err == nil {
|
||||
return NoChangesFound{}
|
||||
}
|
||||
}
|
||||
|
||||
edited := make(map[string]interface{})
|
||||
@@ -285,6 +342,14 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
edited = fixed.(map[string]interface{})
|
||||
}
|
||||
|
||||
// if you want to abort editing a jira issue then
|
||||
// you can add the "abort: true" flag to the document
|
||||
// and we will abort now
|
||||
if val, ok := edited["abort"].(bool); ok && val {
|
||||
log.Info("abort flag found in template, quiting")
|
||||
return fmt.Errorf("abort flag found in template, quiting")
|
||||
}
|
||||
|
||||
if _, ok := templateData["meta"]; ok {
|
||||
mf := templateData["meta"].(map[string]interface{})["fields"]
|
||||
if f, ok := edited["fields"].(map[string]interface{}); ok {
|
||||
@@ -318,7 +383,7 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
}
|
||||
|
||||
func (c *Cli) Browse(issue string) error {
|
||||
if val, ok := c.opts["browse"]; ok && val == "true" {
|
||||
if val, ok := c.opts["browse"].(bool); ok && val {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return exec.Command("open", fmt.Sprintf("%s/browse/%s", c.endpoint, issue)).Run()
|
||||
} else if runtime.GOOS == "linux" {
|
||||
@@ -327,3 +392,110 @@ func (c *Cli) Browse(issue string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) SaveData(data interface{}) error {
|
||||
if val, ok := c.opts["saveFile"].(string); ok && val != "" {
|
||||
yamlWrite(val, data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) ViewIssue(issue string) (interface{}, error) {
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
|
||||
data, err := responseToJson(c.get(uri))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) FindIssues() (interface{}, error) {
|
||||
var query string
|
||||
var ok bool
|
||||
// project = BAKERY and status not in (Resolved, Closed)
|
||||
if query, ok = c.opts["query"].(string); !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 nil, 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"].(string); ok {
|
||||
fields = strings.Split(qf, ",")
|
||||
} else {
|
||||
fields = append(fields, "summary")
|
||||
}
|
||||
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"jql": query,
|
||||
"startAt": "0",
|
||||
"maxResults": c.opts["max_results"],
|
||||
"fields": fields,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/search", c.endpoint)
|
||||
if data, err := responseToJson(c.post(uri, json)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) GetOptString(optName string, dflt string) string {
|
||||
return c.getOptString(optName, dflt)
|
||||
}
|
||||
|
||||
func (c *Cli) getOptString(optName string, dflt string) string {
|
||||
if val, ok := c.opts[optName].(string); ok {
|
||||
return val
|
||||
} else {
|
||||
return dflt
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cli) GetOptBool(optName string, dflt bool) bool {
|
||||
return c.getOptBool(optName, dflt)
|
||||
}
|
||||
|
||||
func (c *Cli) getOptBool(optName string, dflt bool) bool {
|
||||
if val, ok := c.opts[optName].(bool); ok {
|
||||
return val
|
||||
} else {
|
||||
return dflt
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package cli
|
||||
package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.google.com/p/gopass"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"strings"
|
||||
// "github.com/kr/pretty"
|
||||
@@ -14,15 +16,19 @@ 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"]
|
||||
user, _ := c.opts["user"].(string)
|
||||
|
||||
prompt := fmt.Sprintf("Enter Password for %s: ", user)
|
||||
passwd, _ := gopass.GetPass(prompt)
|
||||
fmt.Printf("Enter Password for %s: ", user)
|
||||
pwbytes, _ := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
passwd := string(pwbytes)
|
||||
|
||||
req.SetBasicAuth(user, passwd)
|
||||
log.Info("%s %s", req.Method, req.URL.String())
|
||||
if resp, err := c.makeRequest(req); err != nil {
|
||||
return err
|
||||
} else {
|
||||
out, _ := httputil.DumpResponse(resp, true)
|
||||
log.Debug("%s", out)
|
||||
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
|
||||
@@ -65,82 +71,20 @@ func (c *Cli) CmdFields() error {
|
||||
|
||||
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, ",")
|
||||
if data, err := c.FindIssues(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
fields = append(fields, "summary")
|
||||
return runTemplate(c.getTemplate("list"), data, nil)
|
||||
}
|
||||
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"jql": query,
|
||||
"startAt": "0",
|
||||
"maxResults": c.opts["max_results"],
|
||||
"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))
|
||||
data, err := c.ViewIssue(issue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runTemplate(c.getTemplate("view"), data, nil)
|
||||
}
|
||||
|
||||
@@ -169,6 +113,11 @@ func (c *Cli) CmdEdit(issue string) error {
|
||||
fmt.Sprintf("%s-edit-", issue),
|
||||
issueData,
|
||||
func(json string) error {
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("PUT: %s", json)
|
||||
log.Debug("Dryrun mode, skipping PUT")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.put(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -176,7 +125,9 @@ func (c *Cli) CmdEdit(issue string) error {
|
||||
|
||||
if resp.StatusCode == 204 {
|
||||
c.Browse(issueData["key"].(string))
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueData["key"], c.endpoint, issueData["key"])
|
||||
if !c.opts["quiet"].(bool) {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueData["key"], c.endpoint, issueData["key"])
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
@@ -213,7 +164,8 @@ func (c *Cli) CmdTransitionMeta(issue string) error {
|
||||
return runTemplate(c.getTemplate("transmeta"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdIssueTypes(project string) error {
|
||||
func (c *Cli) CmdIssueTypes() error {
|
||||
project := c.opts["project"].(string)
|
||||
log.Debug("issueTypes called")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s", c.endpoint, project)
|
||||
data, err := responseToJson(c.get(uri))
|
||||
@@ -224,7 +176,10 @@ func (c *Cli) CmdIssueTypes(project string) error {
|
||||
return runTemplate(c.getTemplate("issuetypes"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdCreateMeta(project string, issuetype string) error {
|
||||
func (c *Cli) CmdCreateMeta() error {
|
||||
project := c.opts["project"].(string)
|
||||
issuetype := c.getOptString("issuetype", "Bug")
|
||||
|
||||
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))
|
||||
@@ -246,6 +201,16 @@ func (c *Cli) CmdCreateMeta(project string, issuetype string) error {
|
||||
return runTemplate(c.getTemplate("createmeta"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdComponents(project string) error {
|
||||
log.Debug("Components called")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/project/%s/components", c.endpoint, project)
|
||||
data, err := responseToJson(c.get(uri))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runTemplate(c.getTemplate("components"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdTransitions(issue string) error {
|
||||
log.Debug("Transitions called")
|
||||
c.Browse(issue)
|
||||
@@ -257,7 +222,9 @@ func (c *Cli) CmdTransitions(issue string) error {
|
||||
return runTemplate(c.getTemplate("transitions"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdCreate(project string, issuetype string) error {
|
||||
func (c *Cli) CmdCreate() error {
|
||||
project := c.opts["project"].(string)
|
||||
issuetype := c.getOptString("issuetype", "Bug")
|
||||
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)
|
||||
@@ -268,7 +235,7 @@ func (c *Cli) CmdCreate(project string, issuetype string) error {
|
||||
|
||||
issueData := make(map[string]interface{})
|
||||
issueData["overrides"] = c.opts
|
||||
issueData["overrides"].(map[string]string)["issuetype"] = issuetype
|
||||
issueData["overrides"].(map[string]interface{})["issuetype"] = issuetype
|
||||
|
||||
if val, ok := data.(map[string]interface{})["projects"]; ok {
|
||||
if len(val.([]interface{})) == 0 {
|
||||
@@ -277,6 +244,11 @@ func (c *Cli) CmdCreate(project string, issuetype string) error {
|
||||
return err
|
||||
}
|
||||
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
|
||||
if len(val.([]interface{})) == 0 {
|
||||
err = fmt.Errorf("Project '%s' does not support issuetype '%s'. Unable to create issue.", project, issuetype)
|
||||
log.Error("%s", err)
|
||||
return err
|
||||
}
|
||||
issueData["meta"] = val.([]interface{})[0]
|
||||
}
|
||||
}
|
||||
@@ -289,6 +261,11 @@ func (c *Cli) CmdCreate(project string, issuetype string) error {
|
||||
func(json string) error {
|
||||
log.Debug("JSON: %s", json)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue", c.endpoint)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("POST: %s", json)
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.post(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -299,10 +276,16 @@ func (c *Cli) CmdCreate(project string, issuetype string) error {
|
||||
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)
|
||||
|
||||
key := json.(map[string]interface{})["key"].(string)
|
||||
link := fmt.Sprintf("%s/browse/%s", c.endpoint, key)
|
||||
c.Browse(key)
|
||||
c.SaveData(map[string]string{
|
||||
"issue": key,
|
||||
"link": link,
|
||||
})
|
||||
if !c.opts["quiet"].(bool) {
|
||||
fmt.Printf("OK %s %s\n", key, link)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
@@ -346,13 +329,20 @@ func (c *Cli) CmdBlocks(blocker string, issue string) error {
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issueLink", c.endpoint)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("POST: %s", json)
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
if !c.opts["quiet"].(bool) {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
}
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
@@ -382,13 +372,20 @@ func (c *Cli) CmdDups(duplicate string, issue string) error {
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issueLink", c.endpoint)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("POST: %s", json)
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
if !c.opts["quiet"].(bool) {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
}
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
@@ -399,26 +396,93 @@ func (c *Cli) CmdDups(duplicate string, issue string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdWatch(issue string, watcher string) error {
|
||||
log.Debug("watch called")
|
||||
func (c *Cli) CmdWatch(issue string, watcher string, remove bool) error {
|
||||
log.Debug("watch called: watcher: %q, remove: %n", watcher, remove)
|
||||
|
||||
var uri string
|
||||
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 c.getOptBool("dryrun", false) {
|
||||
if !remove {
|
||||
log.Debug("POST: %s", json)
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
} else {
|
||||
log.Debug("DELETE: %s", watcher)
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
if !remove {
|
||||
uri = fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", c.endpoint, issue)
|
||||
resp, err = c.post(uri, json)
|
||||
} else {
|
||||
uri = fmt.Sprintf("%s/rest/api/2/issue/%s/watchers?username=%s", c.endpoint, issue, watcher)
|
||||
resp, err = c.delete(uri)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == 204 {
|
||||
c.Browse(issue)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
if !c.opts["quiet"].(bool) {
|
||||
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")
|
||||
if !remove {
|
||||
err = fmt.Errorf("Unexpected Response From POST")
|
||||
} else {
|
||||
err = fmt.Errorf("Unexpected Response From DELETE")
|
||||
}
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdVote(issue string, up bool) error {
|
||||
log.Debug("vote called, with up: %n", up)
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", c.endpoint, issue)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
if up {
|
||||
log.Debug("POST: %s", "")
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
} else {
|
||||
log.Debug("DELETE: %s", "")
|
||||
log.Debug("Dryrun mode, skipping DELETE")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var resp *http.Response
|
||||
var err error
|
||||
if up {
|
||||
resp, err = c.post(uri, "")
|
||||
} else {
|
||||
resp, err = c.delete(uri)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == 204 {
|
||||
c.Browse(issue)
|
||||
if !c.opts["quiet"].(bool) {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
}
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
if up {
|
||||
err = fmt.Errorf("Unexpected Response From POST")
|
||||
} else {
|
||||
err = fmt.Errorf("Unexpected Response From DELETE")
|
||||
}
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
@@ -441,7 +505,7 @@ func (c *Cli) CmdTransition(issue string, trans string) error {
|
||||
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) {
|
||||
if strings.Contains(strings.ToLower(name), strings.ToLower(trans)) {
|
||||
transName = name
|
||||
transId = id
|
||||
transMeta = transition.(map[string]interface{})
|
||||
@@ -457,13 +521,20 @@ func (c *Cli) CmdTransition(issue string, trans string) error {
|
||||
log.Debug("POST: %s", json)
|
||||
// os.Exit(0)
|
||||
uri = fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", c.endpoint, issue)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("POST: %s", json)
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
if !c.opts["quiet"].(bool) {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
}
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
@@ -502,6 +573,11 @@ func (c *Cli) CmdComment(issue string) error {
|
||||
handlePost := func(json string) error {
|
||||
log.Debug("JSON: %s", json)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/comment", c.endpoint, issue)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("POST: %s", json)
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.post(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -509,7 +585,9 @@ func (c *Cli) CmdComment(issue string) error {
|
||||
|
||||
if resp.StatusCode == 201 {
|
||||
c.Browse(issue)
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
if !c.opts["quiet"].(bool) {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
@@ -539,6 +617,114 @@ func (c *Cli) CmdComment(issue string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdComponent(action string, project string, name string, desc string, lead string) error {
|
||||
log.Debug("component called")
|
||||
|
||||
switch action {
|
||||
case "add":
|
||||
default:
|
||||
return errors.New(fmt.Sprintf("CmdComponent: %q is not a valid action", action))
|
||||
}
|
||||
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"name": name,
|
||||
"description": desc,
|
||||
"leadUserName": lead,
|
||||
"project": project,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/component", c.endpoint)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("POST: %s", json)
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.post(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == 201 {
|
||||
if !c.opts["quiet"].(bool) {
|
||||
fmt.Printf("OK %s %s\n", project, name)
|
||||
}
|
||||
} 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) CmdLabels(action string, issue string, labels []string) error {
|
||||
log.Debug("label called")
|
||||
|
||||
if action != "add" && action != "remove" && action != "set" {
|
||||
return fmt.Errorf("action must be 'add', 'set' or 'remove': %q is invalid", action)
|
||||
}
|
||||
|
||||
handlePut := func(json string) error {
|
||||
log.Debug("JSON: %s", json)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("PUT: %s", json)
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.put(uri, json)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 204 {
|
||||
c.Browse(issue)
|
||||
if !c.opts["quiet"].(bool) {
|
||||
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 PUT")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var labels_json string
|
||||
var err error
|
||||
if action == "set" {
|
||||
labelsActions := make([]map[string][]string, 1)
|
||||
labelsActions[0] = map[string][]string{
|
||||
"set": labels,
|
||||
}
|
||||
labels_json, err = jsonEncode(map[string]interface{}{
|
||||
"labels": labelsActions,
|
||||
})
|
||||
} else {
|
||||
labelsActions := make([]map[string]string, len(labels))
|
||||
for i, label := range labels {
|
||||
labelActionMap := map[string]string{
|
||||
action: label,
|
||||
}
|
||||
labelsActions[i] = labelActionMap
|
||||
}
|
||||
labels_json, err = jsonEncode(map[string]interface{}{
|
||||
"labels": labelsActions,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
json := fmt.Sprintf("{ \"update\": %s }", labels_json)
|
||||
return handlePut(json)
|
||||
|
||||
}
|
||||
|
||||
func (c *Cli) CmdAssign(issue string, user string) error {
|
||||
log.Debug("assign called")
|
||||
|
||||
@@ -550,13 +736,20 @@ func (c *Cli) CmdAssign(issue string, user string) error {
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/assignee", c.endpoint, issue)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("PUT: %s", json)
|
||||
log.Debug("Dryrun mode, skipping PUT")
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
if !c.opts["quiet"].(bool) {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
|
||||
}
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
@@ -568,7 +761,7 @@ func (c *Cli) CmdAssign(issue string, user string) error {
|
||||
}
|
||||
|
||||
func (c *Cli) CmdExportTemplates() error {
|
||||
dir := c.opts["directory"]
|
||||
dir := c.opts["directory"].(string)
|
||||
if err := mkdir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -593,3 +786,25 @@ func (c *Cli) CmdExportTemplates() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdRequest(uri, content string) (err error) {
|
||||
log.Debug("request called")
|
||||
|
||||
if !strings.HasPrefix(uri, "http") {
|
||||
uri = fmt.Sprintf("%s%s", c.endpoint, uri)
|
||||
}
|
||||
|
||||
method := strings.ToUpper(c.opts["method"].(string))
|
||||
var data interface{}
|
||||
if method == "GET" {
|
||||
data, err = responseToJson(c.get(uri))
|
||||
} else if method == "POST" {
|
||||
data, err = responseToJson(c.post(uri, content))
|
||||
} else if method == "PUT" {
|
||||
data, err = responseToJson(c.put(uri, content))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runTemplate(c.getTemplate("request"), data, nil)
|
||||
}
|
||||
-387
@@ -1,387 +0,0 @@
|
||||
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")
|
||||
defaultMaxResults := "500"
|
||||
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] [--max_results MAX_RESULTS]
|
||||
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
|
||||
--max_results=VAL Maximum number of results to return in query (default: %s)
|
||||
`, user, fmt.Sprintf("%s/.jira.d/templates", home), user, defaultMaxResults)
|
||||
|
||||
args, err := docopt.Parse(usage, nil, true, "0.0.8", 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["max_results"]; !ok {
|
||||
opts["max_results"] = defaultMaxResults
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+523
@@ -0,0 +1,523 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/Netflix-Skunkworks/go-jira"
|
||||
"github.com/coryb/optigo"
|
||||
"github.com/op/go-logging"
|
||||
"gopkg.in/coryb/yaml.v2"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logging.MustGetLogger("jira")
|
||||
format = "%{color}%{time:2006-01-02T15:04:05.000Z07:00} %{level:-5s} [%{shortfile}]%{color:reset} %{message}"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||
logging.SetBackend(
|
||||
logging.NewBackendFormatter(
|
||||
logBackend,
|
||||
logging.MustStringFormatter(format),
|
||||
),
|
||||
)
|
||||
logging.SetLevel(logging.NOTICE, "")
|
||||
|
||||
user := os.Getenv("USER")
|
||||
home := os.Getenv("HOME")
|
||||
defaultQueryFields := "summary,created,updated,priority,status,reporter,assignee"
|
||||
defaultSort := "priority asc, created"
|
||||
defaultMaxResults := 500
|
||||
|
||||
usage := func(ok bool) {
|
||||
printer := fmt.Printf
|
||||
if !ok {
|
||||
printer = func(format string, args ...interface{}) (int, error) {
|
||||
return fmt.Fprintf(os.Stderr, format, args...)
|
||||
}
|
||||
defer func() {
|
||||
os.Exit(1)
|
||||
}()
|
||||
} else {
|
||||
defer func() {
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
output := fmt.Sprintf(`
|
||||
Usage:
|
||||
jira (ls|list) <Query Options>
|
||||
jira view ISSUE
|
||||
jira edit [--noedit] <Edit Options> [ISSUE | <Query Options>]
|
||||
jira create [--noedit] [-p PROJECT] <Create Options>
|
||||
jira DUPLICATE dups ISSUE
|
||||
jira BLOCKER blocks ISSUE
|
||||
jira vote ISSUE [--down]
|
||||
jira watch ISSUE [-w WATCHER] [--remove]
|
||||
jira (trans|transition) TRANSITION ISSUE [--noedit] <Edit Options>
|
||||
jira ack ISSUE [--edit] <Edit Options>
|
||||
jira close ISSUE [--edit] <Edit Options>
|
||||
jira resolve ISSUE [--edit] <Edit Options>
|
||||
jira reopen ISSUE [--edit] <Edit Options>
|
||||
jira start ISSUE [--edit] <Edit Options>
|
||||
jira stop ISSUE [--edit] <Edit Options>
|
||||
jira comment ISSUE [--noedit] <Edit Options>
|
||||
jira (set,add,remove) labels ISSUE [LABEL] ...
|
||||
jira take ISSUE
|
||||
jira (assign|give) ISSUE ASSIGNEE
|
||||
jira fields
|
||||
jira issuelinktypes
|
||||
jira transmeta ISSUE
|
||||
jira editmeta ISSUE
|
||||
jira add component [-p PROJECT] NAME DESCRIPTION LEAD
|
||||
jira components [-p PROJECT]
|
||||
jira issuetypes [-p PROJECT]
|
||||
jira createmeta [-p PROJECT] [-i ISSUETYPE]
|
||||
jira transitions ISSUE
|
||||
jira export-templates [-d DIR] [-t template]
|
||||
jira (b|browse) ISSUE
|
||||
jira login
|
||||
jira request [-M METHOD] URI [DATA]
|
||||
jira ISSUE
|
||||
|
||||
General Options:
|
||||
-b --browse Open your browser to the Jira issue
|
||||
-e --endpoint=URI URI to use for jira
|
||||
-k --insecure disable TLS certificate verification
|
||||
-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 Print version
|
||||
|
||||
Query Options:
|
||||
-a --assignee=USER Username assigned the issue
|
||||
-c --component=COMPONENT Component to Search for
|
||||
-f --queryfields=FIELDS Fields that are used in "list" template: (default: %s)
|
||||
-i --issuetype=ISSUETYPE The Issue Type
|
||||
-l --limit=VAL Maximum number of results to return in query (default: %d)
|
||||
-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: %s)
|
||||
-w --watcher=USER Watcher to add to issue (default: %s)
|
||||
or Watcher to search for
|
||||
|
||||
Edit Options:
|
||||
-m --comment=COMMENT Comment message for transition
|
||||
-o --override=KEY=VAL Set custom key/value pairs
|
||||
|
||||
Create Options:
|
||||
-i --issuetype=ISSUETYPE Jira Issue Type (default: Bug)
|
||||
-m --comment=COMMENT Comment message for transition
|
||||
-o --override=KEY=VAL Set custom key/value pairs
|
||||
|
||||
Command Options:
|
||||
-d --directory=DIR Directory to export templates to (default: %s)
|
||||
`, user, defaultQueryFields, defaultMaxResults, defaultSort, user, fmt.Sprintf("%s/.jira.d/templates", home))
|
||||
printer(output)
|
||||
}
|
||||
|
||||
jiraCommands := map[string]string{
|
||||
"list": "list",
|
||||
"ls": "list",
|
||||
"view": "view",
|
||||
"edit": "edit",
|
||||
"create": "create",
|
||||
"dups": "dups",
|
||||
"blocks": "blocks",
|
||||
"watch": "watch",
|
||||
"trans": "transition",
|
||||
"transition": "transition",
|
||||
"ack": "acknowledge",
|
||||
"acknowledge": "acknowledge",
|
||||
"close": "close",
|
||||
"resolve": "resolve",
|
||||
"reopen": "reopen",
|
||||
"start": "start",
|
||||
"stop": "stop",
|
||||
"comment": "comment",
|
||||
"label": "labels",
|
||||
"labels": "labels",
|
||||
"component": "component",
|
||||
"components": "components",
|
||||
"take": "take",
|
||||
"assign": "assign",
|
||||
"give": "assign",
|
||||
"fields": "fields",
|
||||
"issuelinktypes": "issuelinktypes",
|
||||
"transmeta": "transmeta",
|
||||
"editmeta": "editmeta",
|
||||
"issuetypes": "issuetypes",
|
||||
"createmeta": "createmeta",
|
||||
"transitions": "transitions",
|
||||
"export-templates": "export-templates",
|
||||
"browse": "browse",
|
||||
"login": "login",
|
||||
"req": "request",
|
||||
"request": "request",
|
||||
"vote": "vote",
|
||||
}
|
||||
|
||||
defaults := map[string]interface{}{
|
||||
"user": user,
|
||||
"queryfields": defaultQueryFields,
|
||||
"directory": fmt.Sprintf("%s/.jira.d/templates", home),
|
||||
"sort": defaultSort,
|
||||
"max_results": defaultMaxResults,
|
||||
"method": "GET",
|
||||
"quiet": false,
|
||||
}
|
||||
opts := make(map[string]interface{})
|
||||
|
||||
setopt := func(name string, value interface{}) {
|
||||
opts[name] = value
|
||||
}
|
||||
|
||||
op := optigo.NewDirectAssignParser(map[string]interface{}{
|
||||
"h|help": usage,
|
||||
"version": func() {
|
||||
fmt.Println(fmt.Sprintf("version: %s", jira.VERSION))
|
||||
os.Exit(0)
|
||||
},
|
||||
"v|verbose+": func() {
|
||||
logging.SetLevel(logging.GetLevel("")+1, "")
|
||||
},
|
||||
"dryrun": setopt,
|
||||
"b|browse": setopt,
|
||||
"editor=s": setopt,
|
||||
"u|user=s": setopt,
|
||||
"endpoint=s": setopt,
|
||||
"k|insecure": setopt,
|
||||
"t|template=s": setopt,
|
||||
"q|query=s": setopt,
|
||||
"p|project=s": setopt,
|
||||
"c|component=s": setopt,
|
||||
"a|assignee=s": setopt,
|
||||
"i|issuetype=s": setopt,
|
||||
"w|watcher=s": setopt,
|
||||
"remove": setopt,
|
||||
"r|reporter=s": setopt,
|
||||
"f|queryfields=s": setopt,
|
||||
"s|sort=s": setopt,
|
||||
"l|limit|max_results=i": setopt,
|
||||
"o|override=s%": &opts,
|
||||
"noedit": setopt,
|
||||
"edit": setopt,
|
||||
"m|comment=s": setopt,
|
||||
"d|dir|directory=s": setopt,
|
||||
"M|method=s": setopt,
|
||||
"S|saveFile=s": setopt,
|
||||
"Q|quiet": setopt,
|
||||
"down": setopt,
|
||||
})
|
||||
|
||||
if err := op.ProcessAll(os.Args[1:]); err != nil {
|
||||
log.Error("%s", err)
|
||||
usage(false)
|
||||
}
|
||||
args := op.Args
|
||||
|
||||
var command string
|
||||
if len(args) > 0 {
|
||||
if alias, ok := jiraCommands[args[0]]; ok {
|
||||
command = alias
|
||||
args = args[1:]
|
||||
} else if len(args) > 1 {
|
||||
// look at second arg for "dups" and "blocks" commands
|
||||
// also for 'set/add/remove' actions like 'labels'
|
||||
if alias, ok := jiraCommands[args[1]]; ok {
|
||||
command = alias
|
||||
args = append(args[:1], args[2:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if command == "" && len(args) > 0 {
|
||||
command = args[0]
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
os.Setenv("JIRA_OPERATION", command)
|
||||
loadConfigs(opts)
|
||||
|
||||
// check to see if it was set in the configs:
|
||||
if value, ok := opts["command"].(string); ok {
|
||||
command = value
|
||||
} else if _, ok := jiraCommands[command]; !ok || command == "" {
|
||||
if command != "" {
|
||||
args = append([]string{command}, args...)
|
||||
}
|
||||
command = "view"
|
||||
}
|
||||
|
||||
// apply defaults
|
||||
for k, v := range defaults {
|
||||
if _, ok := opts[k]; !ok {
|
||||
log.Debug("Setting %q to %#v from defaults", k, v)
|
||||
opts[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("opts: %v", opts)
|
||||
log.Debug("args: %v", args)
|
||||
|
||||
if _, ok := opts["endpoint"]; !ok {
|
||||
log.Error("endpoint option required. Either use --endpoint or set a endpoint option in your ~/.jira.d/config.yml file")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c := jira.New(opts)
|
||||
|
||||
log.Debug("opts: %s", opts)
|
||||
|
||||
setEditing := func(dflt bool) {
|
||||
log.Debug("Default Editing: %t", dflt)
|
||||
if dflt {
|
||||
if val, ok := opts["noedit"].(bool); ok && val {
|
||||
log.Debug("Setting edit = false")
|
||||
opts["edit"] = false
|
||||
} else {
|
||||
log.Debug("Setting edit = true")
|
||||
opts["edit"] = true
|
||||
}
|
||||
} else {
|
||||
if _, ok := opts["edit"].(bool); !ok {
|
||||
log.Debug("Setting edit = %t", dflt)
|
||||
opts["edit"] = dflt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requireArgs := func(count int) {
|
||||
if len(args) < count {
|
||||
log.Error("Not enough arguments. %d required, %d provided", count, len(args))
|
||||
usage(false)
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
switch command {
|
||||
case "login":
|
||||
err = c.CmdLogin()
|
||||
case "fields":
|
||||
err = c.CmdFields()
|
||||
case "list":
|
||||
err = c.CmdList()
|
||||
case "edit":
|
||||
setEditing(true)
|
||||
if len(args) > 0 {
|
||||
err = c.CmdEdit(args[0])
|
||||
} else {
|
||||
var data interface{}
|
||||
if data, err = c.FindIssues(); err == nil {
|
||||
issues := data.(map[string]interface{})["issues"].([]interface{})
|
||||
for _, issue := range issues {
|
||||
if err = c.CmdEdit(issue.(map[string]interface{})["key"].(string)); err != nil {
|
||||
switch err.(type) {
|
||||
case jira.NoChangesFound:
|
||||
log.Warning("No Changes found: %s", err)
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "editmeta":
|
||||
requireArgs(1)
|
||||
err = c.CmdEditMeta(args[0])
|
||||
case "transmeta":
|
||||
requireArgs(1)
|
||||
err = c.CmdTransitionMeta(args[0])
|
||||
case "issuelinktypes":
|
||||
err = c.CmdIssueLinkTypes()
|
||||
case "issuetypes":
|
||||
err = c.CmdIssueTypes()
|
||||
case "createmeta":
|
||||
err = c.CmdCreateMeta()
|
||||
case "create":
|
||||
setEditing(true)
|
||||
err = c.CmdCreate()
|
||||
case "transitions":
|
||||
requireArgs(1)
|
||||
err = c.CmdTransitions(args[0])
|
||||
case "blocks":
|
||||
requireArgs(2)
|
||||
err = c.CmdBlocks(args[0], args[1])
|
||||
case "dups":
|
||||
requireArgs(2)
|
||||
if err = c.CmdDups(args[0], args[1]); err == nil {
|
||||
opts["resolution"] = "Duplicate"
|
||||
err = c.CmdTransition(args[0], "close")
|
||||
}
|
||||
case "watch":
|
||||
requireArgs(1)
|
||||
watcher := c.GetOptString("watcher", opts["user"].(string))
|
||||
remove := c.GetOptBool("remove", false)
|
||||
err = c.CmdWatch(args[0], watcher, remove)
|
||||
case "transition":
|
||||
requireArgs(2)
|
||||
setEditing(true)
|
||||
err = c.CmdTransition(args[1], args[0])
|
||||
case "close":
|
||||
requireArgs(1)
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args[0], "close")
|
||||
case "acknowledge":
|
||||
requireArgs(1)
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args[0], "acknowledge")
|
||||
case "reopen":
|
||||
requireArgs(1)
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args[0], "reopen")
|
||||
case "resolve":
|
||||
requireArgs(1)
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args[0], "resolve")
|
||||
case "start":
|
||||
requireArgs(1)
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args[0], "start")
|
||||
case "stop":
|
||||
requireArgs(1)
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args[0], "stop")
|
||||
case "comment":
|
||||
requireArgs(1)
|
||||
setEditing(true)
|
||||
err = c.CmdComment(args[0])
|
||||
case "labels":
|
||||
requireArgs(2)
|
||||
action := args[0]
|
||||
issue := args[1]
|
||||
labels := args[2:]
|
||||
err = c.CmdLabels(action, issue, labels)
|
||||
case "component":
|
||||
requireArgs(2)
|
||||
action := args[0]
|
||||
project := opts["project"].(string)
|
||||
name := args[1]
|
||||
var lead string
|
||||
var description string
|
||||
if len(args) > 2 {
|
||||
description = args[2]
|
||||
}
|
||||
if len(args) > 3 {
|
||||
lead = args[2]
|
||||
}
|
||||
err = c.CmdComponent(action, project, name, description, lead)
|
||||
case "components":
|
||||
project := opts["project"].(string)
|
||||
err = c.CmdComponents(project)
|
||||
case "take":
|
||||
requireArgs(1)
|
||||
err = c.CmdAssign(args[0], opts["user"].(string))
|
||||
case "browse":
|
||||
requireArgs(1)
|
||||
opts["browse"] = true
|
||||
err = c.Browse(args[0])
|
||||
case "export-templates":
|
||||
err = c.CmdExportTemplates()
|
||||
case "assign":
|
||||
requireArgs(2)
|
||||
err = c.CmdAssign(args[0], args[1])
|
||||
case "view":
|
||||
requireArgs(1)
|
||||
err = c.CmdView(args[0])
|
||||
case "vote":
|
||||
requireArgs(1)
|
||||
if val, ok := opts["down"]; ok {
|
||||
err = c.CmdVote(args[0], !val.(bool))
|
||||
} else {
|
||||
err = c.CmdVote(args[0], true)
|
||||
}
|
||||
case "request":
|
||||
requireArgs(1)
|
||||
data := ""
|
||||
if len(args) > 1 {
|
||||
data = args[1]
|
||||
}
|
||||
err = c.CmdRequest(args[0], data)
|
||||
default:
|
||||
log.Error("Unknown command %s", command)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func parseYaml(file string, opts map[string]interface{}) {
|
||||
if fh, err := ioutil.ReadFile(file); err == nil {
|
||||
log.Debug("Found Config file: %s", file)
|
||||
yaml.Unmarshal(fh, &opts)
|
||||
}
|
||||
}
|
||||
|
||||
func populateEnv(opts map[string]interface{}) {
|
||||
for k, v := range opts {
|
||||
envName := fmt.Sprintf("JIRA_%s", strings.ToUpper(k))
|
||||
var val string
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
val = t
|
||||
case int, int8, int16, int32, int64:
|
||||
val = fmt.Sprintf("%d", t)
|
||||
case float32, float64:
|
||||
val = fmt.Sprintf("%f", t)
|
||||
case bool:
|
||||
val = fmt.Sprintf("%t", t)
|
||||
default:
|
||||
val = fmt.Sprintf("%v", t)
|
||||
}
|
||||
os.Setenv(envName, val)
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigs(opts map[string]interface{}) {
|
||||
populateEnv(opts)
|
||||
paths := jira.FindParentPaths(".jira.d/config.yml")
|
||||
// prepend
|
||||
paths = append([]string{"/etc/go-jira.yml"}, paths...)
|
||||
|
||||
// iterate paths in reverse
|
||||
for i := len(paths) - 1; i >= 0; i-- {
|
||||
file := paths[i]
|
||||
if stat, err := os.Stat(file); err == nil {
|
||||
tmp := make(map[string]interface{})
|
||||
// check to see if config file is exectuable
|
||||
if stat.Mode()&0111 == 0 {
|
||||
parseYaml(file, tmp)
|
||||
} 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(), &tmp)
|
||||
}
|
||||
for k, v := range tmp {
|
||||
if _, ok := opts[k]; !ok {
|
||||
log.Debug("Setting %q to %#v from %s", k, v, file)
|
||||
opts[k] = v
|
||||
}
|
||||
}
|
||||
populateEnv(opts)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cli
|
||||
package jira
|
||||
|
||||
var all_templates = map[string]string{
|
||||
"debug": default_debug_template,
|
||||
@@ -12,10 +12,12 @@ var all_templates = map[string]string{
|
||||
"view": default_view_template,
|
||||
"edit": default_edit_template,
|
||||
"transitions": default_transitions_template,
|
||||
"components": default_components_template,
|
||||
"issuetypes": default_issuetypes_template,
|
||||
"create": default_create_template,
|
||||
"comment": default_comment_template,
|
||||
"transition": default_transition_template,
|
||||
"request": default_debug_template,
|
||||
}
|
||||
|
||||
const default_debug_template = "{{ . | toJson}}\n"
|
||||
@@ -50,10 +52,11 @@ comments:
|
||||
{{ or .body "" | indent 4}}
|
||||
{{end}}
|
||||
`
|
||||
const default_edit_template = `update:
|
||||
const default_edit_template = `# issue: {{ .key }}
|
||||
update:
|
||||
comment:
|
||||
- add:
|
||||
body: |
|
||||
body: |~
|
||||
{{ or .overrides.comment "" | indent 10 }}
|
||||
fields:
|
||||
summary: {{ or .overrides.summary .fields.summary }}
|
||||
@@ -70,12 +73,19 @@ fields:
|
||||
- name: {{ .overrides.watcher}}{{end}}
|
||||
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
|
||||
name: {{ or .overrides.priority .fields.priority.name }}
|
||||
description: |
|
||||
description: |~
|
||||
{{ or .overrides.description (or .fields.description "") | indent 4 }}
|
||||
# comments:
|
||||
# {{ range .fields.comment.comments }} - | # {{.author.name}} at {{.created}}
|
||||
# {{ or .body "" | indent 4 | comment}}
|
||||
# {{end}}
|
||||
`
|
||||
const default_transitions_template = `{{ range .transitions }}{{.id }}: {{.name}}
|
||||
{{end}}`
|
||||
|
||||
const default_components_template = `{{ range . }}{{.id }}: {{.name}}
|
||||
{{end}}`
|
||||
|
||||
const default_issuetypes_template = `{{ range .projects }}{{ range .issuetypes }}{{color "+bh"}}{{.name | append ":" | printf "%-13s" }}{{color "reset"}} {{.description}}
|
||||
{{end}}{{end}}`
|
||||
|
||||
@@ -89,7 +99,7 @@ const default_create_template = `fields:
|
||||
name: {{ or .overrides.priority "unassigned" }}
|
||||
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}}
|
||||
- name: {{ . }}{{end}}
|
||||
description: |
|
||||
description: |~
|
||||
{{ or .overrides.description "" | indent 4 }}
|
||||
assignee:
|
||||
name: {{ or .overrides.assignee "" }}
|
||||
@@ -101,14 +111,14 @@ const default_create_template = `fields:
|
||||
- name:
|
||||
`
|
||||
|
||||
const default_comment_template = `body: |
|
||||
const default_comment_template = `body: |~
|
||||
{{ or .overrides.comment "" | indent 2 }}
|
||||
`
|
||||
|
||||
const default_transition_template = `update:
|
||||
comment:
|
||||
- add:
|
||||
body: |
|
||||
body: |~
|
||||
{{ or .overrides.comment "" | indent 10 }}
|
||||
fields:{{if .meta.fields.assignee}}
|
||||
assignee:
|
||||
+78
-10
@@ -1,4 +1,4 @@
|
||||
package cli
|
||||
package jira
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mgutz/ansi"
|
||||
"gopkg.in/coryb/yaml.v2"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -63,6 +64,21 @@ func readFile(file string) string {
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) (err error) {
|
||||
var s, d *os.File
|
||||
if s, err = os.Open(src); err == nil {
|
||||
defer s.Close()
|
||||
if d, err = os.Create(dst); err == nil {
|
||||
if _, err = io.Copy(d, s); err != nil {
|
||||
d.Close()
|
||||
return
|
||||
}
|
||||
return d.Close()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fuzzyAge(start string) (string, error) {
|
||||
if t, err := time.Parse("2006-01-02T15:04:05.000-0700", start); err != nil {
|
||||
return "", err
|
||||
@@ -85,8 +101,19 @@ func fuzzyAge(start string) (string, error) {
|
||||
return "unknown", nil
|
||||
}
|
||||
|
||||
func runTemplate(templateContent string, data interface{}, out io.Writer) error {
|
||||
func dateFormat(format string, content string) (string, error) {
|
||||
if t, err := time.Parse("2006-01-02T15:04:05.000-0700", content); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return t.Format(format), nil
|
||||
}
|
||||
}
|
||||
|
||||
func RunTemplate(templateContent string, data interface{}, out io.Writer) error {
|
||||
return runTemplate(templateContent, data, out)
|
||||
}
|
||||
|
||||
func runTemplate(templateContent string, data interface{}, out io.Writer) error {
|
||||
if out == nil {
|
||||
out = os.Stdout
|
||||
}
|
||||
@@ -110,12 +137,26 @@ func runTemplate(templateContent string, data interface{}, out io.Writer) error
|
||||
}
|
||||
},
|
||||
"indent": func(spaces int, content string) string {
|
||||
indent := make([]byte, spaces+1, spaces+1)
|
||||
indent := make([]rune, 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)
|
||||
|
||||
lineSeps := []rune{'\n', '\u0085', '\u2028', '\u2029'}
|
||||
for _, sep := range lineSeps {
|
||||
indent[0] = sep
|
||||
content = strings.Replace(content, string(sep), string(indent), -1)
|
||||
}
|
||||
return content
|
||||
|
||||
},
|
||||
"comment": func(content string) string {
|
||||
lineSeps := []rune{'\n', '\u0085', '\u2028', '\u2029'}
|
||||
for _, sep := range lineSeps {
|
||||
content = strings.Replace(content, string(sep), string([]rune{sep, '#', ' '}), -1)
|
||||
}
|
||||
return content
|
||||
},
|
||||
"color": func(color string) string {
|
||||
return ansi.ColorCode(color)
|
||||
@@ -123,6 +164,13 @@ func runTemplate(templateContent string, data interface{}, out io.Writer) error
|
||||
"split": func(sep string, content string) []string {
|
||||
return strings.Split(content, sep)
|
||||
},
|
||||
"join": func(sep string, content []interface{}) string {
|
||||
vals := make([]string, len(content))
|
||||
for i, v := range content {
|
||||
vals[i] = v.(string)
|
||||
}
|
||||
return strings.Join(vals, sep)
|
||||
},
|
||||
"abbrev": func(max int, content string) string {
|
||||
if len(content) > max {
|
||||
var buffer bytes.Buffer
|
||||
@@ -142,6 +190,9 @@ func runTemplate(templateContent string, data interface{}, out io.Writer) error
|
||||
"age": func(content string) (string, error) {
|
||||
return fuzzyAge(content)
|
||||
},
|
||||
"dateFormat": func(format string, content string) (string, error) {
|
||||
return dateFormat(format, content)
|
||||
},
|
||||
}
|
||||
if tmpl, err := template.New("template").Funcs(funcs).Parse(templateContent); err != nil {
|
||||
log.Error("Failed to parse template: %s", err)
|
||||
@@ -205,6 +256,21 @@ func jsonWrite(file string, data interface{}) {
|
||||
enc.Encode(data)
|
||||
}
|
||||
|
||||
func yamlWrite(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)
|
||||
}
|
||||
if out, err := yaml.Marshal(data); err != nil {
|
||||
log.Error("Failed to marshal yaml %v: %s", data, err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fh.Write(out)
|
||||
}
|
||||
}
|
||||
|
||||
func promptYN(prompt string, yes bool) bool {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
if !yes {
|
||||
@@ -246,25 +312,27 @@ func yamlFixup(data interface{}) (interface{}, error) {
|
||||
}
|
||||
return copy, nil
|
||||
case map[string]interface{}:
|
||||
copy := make(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
|
||||
copy[k] = fixed
|
||||
}
|
||||
}
|
||||
return d, nil
|
||||
return copy, nil
|
||||
case []interface{}:
|
||||
for i, val := range d {
|
||||
copy := make([]interface{}, 0, len(d))
|
||||
for _, val := range d {
|
||||
if fixed, err := yamlFixup(val); err != nil {
|
||||
return nil, err
|
||||
} else if fixed != nil {
|
||||
d[i] = fixed
|
||||
copy = append(copy, fixed)
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
return copy, nil
|
||||
case string:
|
||||
if d == "" {
|
||||
if d == "" || d == "\n" {
|
||||
return nil, nil
|
||||
}
|
||||
return d, nil
|
||||
Reference in New Issue
Block a user