mirror of
https://github.com/Threnklyn/jira.git
synced 2026-05-19 20:53:27 +02:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| adc08935b4 | |||
| 073c8a3694 | |||
| c4a31a498e | |||
| bcad37089a | |||
| b2ba8de15d | |||
| 6016bda571 | |||
| 34ca09cf1a | |||
| d7fb88ee41 | |||
| de4fe76fec | |||
| 5b870cb7a2 | |||
| 89bb82b3f2 | |||
| dd0f5efd32 | |||
| 68b5e60dd9 | |||
| 71acc5d7fc | |||
| 4f91cecf25 | |||
| 688b987895 | |||
| 71bb04fabb | |||
| 3a9f763f9d | |||
| d86d85f7b2 | |||
| 4b798cbfb4 | |||
| 598924b51d | |||
| 674957af5d | |||
| c568d7e921 | |||
| 6eb3567ca5 | |||
| 87ec73c5c3 | |||
| 23551abb11 | |||
| 693e1441f7 | |||
| 6e5cc9821e | |||
| 9e90376816 | |||
| 20b32c2ed6 | |||
| ac170e9ab1 | |||
| d8bce08d3a | |||
| 382bf4faeb | |||
| 595a5212b4 | |||
| f595801202 | |||
| 404caf6400 | |||
| f7eb04e36d | |||
| b0d4f7273d | |||
| a927181db1 | |||
| b5417ef585 | |||
| c5af781c41 | |||
| f2c4df9b3e | |||
| 1dde7e06e6 | |||
| 7bc1897792 | |||
| 37aab3580b | |||
| ff56136937 | |||
| 42990d8ca0 | |||
| e58625b00c | |||
| 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 |
@@ -5,3 +5,5 @@ src/github.com/docopt/
|
||||
src/github.com/mgutz/
|
||||
src/github.com/op/
|
||||
src/gopkg.in/
|
||||
jira
|
||||
jira.exe
|
||||
@@ -1,5 +1,87 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.3 - 2016-07-30
|
||||
|
||||
* [[#43](https://github.com/Netflix-Skunkworks/go-jira/issues/43)] add support for jira done|todo|prog commands [Cory Bennett] [[dd7d1cc](https://github.com/Netflix-Skunkworks/go-jira/commit/dd7d1cc)]
|
||||
* Reporter is not generally editable. [Mike Pountney] [[a637b43](https://github.com/Netflix-Skunkworks/go-jira/commit/a637b43)]
|
||||
|
||||
## 0.1.2 - 2016-06-29
|
||||
|
||||
* [[#44](https://github.com/Netflix-Skunkworks/go-jira/issues/44)] Close tmpfile before rename to work around "The process cannot access the file because it is being used by another process" error on windows. [Cory Bennett] [[0980f8e](https://github.com/Netflix-Skunkworks/go-jira/commit/0980f8e)]
|
||||
|
||||
## 0.1.1 - 2016-06-28
|
||||
|
||||
* use USERPROFILE instead of HOME for windows, rework paths to use filepath.Join for better cross platform support [Cory Bennett] [[adcedc4](https://github.com/Netflix-Skunkworks/go-jira/commit/adcedc4)]
|
||||
* Include templates from a system path [Mike Pountney] [[cf10f53](https://github.com/Netflix-Skunkworks/go-jira/commit/cf10f53)]
|
||||
* Added support for the ```expand``` option for Issues [tobyjoe] [[fb4afc9](https://github.com/Netflix-Skunkworks/go-jira/commit/fb4afc9)]
|
||||
* change for api changes to go-logging [Cory Bennett] [[7bfc6e8](https://github.com/Netflix-Skunkworks/go-jira/commit/7bfc6e8)]
|
||||
* Fix issuetype calls adding URL escaping [Jonathan Wright] [[e4a25e2](https://github.com/Netflix-Skunkworks/go-jira/commit/e4a25e2)]
|
||||
|
||||
## 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)]
|
||||
|
||||
@@ -1,67 +1,100 @@
|
||||
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)
|
||||
# freebsd-386 \
|
||||
# freebsd-arm \
|
||||
# linux-arm \
|
||||
# openbsd-386 \
|
||||
# openbsd-amd64 \
|
||||
# darwin-386
|
||||
|
||||
build:
|
||||
cd src/github.com/Netflix-Skunkworks/go-jira/jira; \
|
||||
go get -v
|
||||
NAME=jira
|
||||
|
||||
OS=$(shell uname -s)
|
||||
ifeq ($(filter CYGWIN%,$(OS)),$(OS))
|
||||
export CWD=$(shell cygpath -wa .)
|
||||
export SEP=\\
|
||||
export CYGWIN=winsymlinks:native
|
||||
BIN ?= $(GOBIN)$(SEP)$(NAME).exe
|
||||
else
|
||||
export CWD=$(shell pwd)
|
||||
export SEP=/
|
||||
BIN ?= $(GOBIN)$(SEP)$(NAME)
|
||||
endif
|
||||
|
||||
export GOPATH=$(CWD)
|
||||
|
||||
DIST=$(CWD)$(SEP)dist
|
||||
|
||||
GOBIN ?= $(CWD)
|
||||
|
||||
CURVER ?= $(patsubst v%,%,$(shell [ -d .git ] && git describe --abbrev=0 --tags || grep ^\#\# CHANGELOG.md | awk '{print $$2; exit}'))
|
||||
LDFLAGS:=-X jira.VERSION=$(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 '$(GOPATH)' $@
|
||||
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; \
|
||||
echo Building for $$p"; \
|
||||
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
|
||||
for x in $(DIST)/jira-windows-*; do mv $$x $$x.exe; done
|
||||
|
||||
fmt:
|
||||
gofmt -s -w jira
|
||||
gofmt -s -w main/*.go *.go
|
||||
|
||||
install:
|
||||
export GOBIN=~/bin && ${MAKE} build
|
||||
${MAKE} GOBIN=~/bin build
|
||||
|
||||
CURVER ?= $(shell git fetch --tags && git tag | tail -1)
|
||||
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 ^v$(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; \
|
||||
tail -n +2 CHANGELOG.md >> CHANGELOG.md.new; \
|
||||
mv CHANGELOG.md.new CHANGELOG.md; \
|
||||
git commit -m "Updated Changelog" CHANGELOG.md; \
|
||||
git tag $(NEWVER)
|
||||
git tag v$(NEWVER)
|
||||
|
||||
version:
|
||||
@echo $(CURVER)
|
||||
|
||||
clean:
|
||||
rm -rf pkg dist bin && find src \! -path \*/go-jira\* -delete
|
||||
rm -rf pkg dist bin src ./$(NAME)
|
||||
|
||||
@@ -69,25 +69,13 @@ You can download one of the pre-built binaries for **go-jira** [here](https://gi
|
||||
|
||||
* **NOTE** You will need **`go-1.4.1`** minimum
|
||||
|
||||
* If you do not have a **GOPATH** setup, these are simple build steps:
|
||||
|
||||
* To build the `jira` binary the current directory just run:
|
||||
```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
|
||||
make
|
||||
```
|
||||
|
||||
* 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
|
||||
* To install the binary to you ~/bin directory you can run:
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -117,6 +105,8 @@ endpoint: https://jira.mycompany.com
|
||||
EOM
|
||||
```
|
||||
|
||||
Then use `jira login` to authenticate yourself.
|
||||
|
||||
### 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:
|
||||
@@ -167,6 +157,26 @@ When running a command like `jira edit` it will look through the current directo
|
||||
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/**.
|
||||
|
||||
#### Writing/Editing Templates
|
||||
|
||||
First the basic templating functionality is defined by the Go language 'text/template' library. The library reference documentation can be found [here](https://golang.org/pkg/text/template/), and there is a good primer document [here](https://gohugo.io/templates/go-templates/). `go-jira` also provides a few extra helper functions to make it a bit easlier to format the data, those functions are defined [here](https://github.com/Netflix-Skunkworks/go-jira/blob/master/util.go#L133).
|
||||
|
||||
Knowing what data and fields are available to any given template is not obvious. The easiest approach to determine what is available is to use the `debug` template on any given operation. For eample to find out what is available to the "view" templates, you can use:
|
||||
```
|
||||
jira view GOJIRA-321 -t debug
|
||||
```
|
||||
|
||||
This will print out the data in JSON format that is available to the template. You can do this for any other operation, like "list":
|
||||
```
|
||||
jira list -t debug
|
||||
```
|
||||
|
||||
Figuring out what is available to input templates (like for the `create` operation) is a bit more tricky, but similar. To find the data available for a `create` template you can run:
|
||||
```
|
||||
jira create --dryrun -t debug --editor /bin/cat
|
||||
```
|
||||
This will attempt to fetch metadata for your default project (you can provide any options that you would normally specify for the `create` operation). It uses the `--dryrun` option to prevent any actual updates being sent to Jira. The `-t debug` is like before to cause the input to be serialized to JSON and printed for your inspection. Finally the `--editor /bin/cat` will cause `go-jira` to just print the template rather than open up an editor and wait for you to edit/save it.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
@@ -205,13 +215,13 @@ 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: $USER)
|
||||
-u --user=USER Username to use for authentication (default: $USER)
|
||||
-v --verbose Increase output logging
|
||||
|
||||
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: summary,created,priority,status,reporter,assignee)
|
||||
-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
|
||||
|
||||
+138
-47
@@ -1,24 +1,30 @@
|
||||
package cli
|
||||
package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/op/go-logging"
|
||||
"gopkg.in/coryb/yaml.v2"
|
||||
"gopkg.in/op/go-logging.v1"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var log = logging.MustGetLogger("jira.cli")
|
||||
var (
|
||||
log = logging.MustGetLogger("jira")
|
||||
VERSION string
|
||||
)
|
||||
|
||||
type Cli struct {
|
||||
endpoint *url.URL
|
||||
@@ -28,20 +34,31 @@ type Cli struct {
|
||||
}
|
||||
|
||||
func New(opts map[string]interface{}) *Cli {
|
||||
homedir := os.Getenv("HOME")
|
||||
homedir := homedir()
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
endpoint, _ := opts["endpoint"].(string)
|
||||
url, _ := url.Parse(strings.TrimRight(endpoint, "/"))
|
||||
|
||||
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},
|
||||
cookieFile: filepath.Join(homedir, ".jira.d", "cookies.js"),
|
||||
ua: &http.Client{
|
||||
Jar: cookieJar,
|
||||
Transport: transport,
|
||||
},
|
||||
}
|
||||
|
||||
cli.ua.Jar.SetCookies(url, cli.loadCookies())
|
||||
@@ -72,6 +89,7 @@ func (c *Cli) saveCookies(cookies []*http.Cookie) {
|
||||
}
|
||||
jsonWrite(c.cookieFile, mergedCookies)
|
||||
} else {
|
||||
mkdir(path.Dir(c.cookieFile))
|
||||
jsonWrite(c.cookieFile, cookies)
|
||||
}
|
||||
}
|
||||
@@ -83,15 +101,15 @@ func (c *Cli) loadCookies() []*http.Cookie {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Failed to open %s: %s", c.cookieFile, err)
|
||||
log.Errorf("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.Errorf("Failed to parse json from file %s: %s", c.cookieFile, err)
|
||||
}
|
||||
log.Debug("Loading Cookies: %s", cookies)
|
||||
log.Debugf("Loading Cookies: %s", cookies)
|
||||
return cookies
|
||||
}
|
||||
|
||||
@@ -103,15 +121,33 @@ 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.Infof("%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)
|
||||
|
||||
log.Info("%s %s", req.Method, req.URL.String())
|
||||
log.Infof("%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)
|
||||
log.Debugf("%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))
|
||||
@@ -133,11 +169,11 @@ func (c *Cli) makeRequestWithContent(method string, uri string, content string)
|
||||
|
||||
func (c *Cli) get(uri string) (*http.Response, error) {
|
||||
req, _ := http.NewRequest("GET", uri, nil)
|
||||
log.Info("%s %s", req.Method, req.URL.String())
|
||||
log.Infof("%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)
|
||||
log.Debugf("%s", logBuffer)
|
||||
}
|
||||
|
||||
if resp, err := c.makeRequest(req); err != nil {
|
||||
@@ -156,11 +192,11 @@ func (c *Cli) get(uri string) (*http.Response, error) {
|
||||
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)
|
||||
log.Errorf("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)
|
||||
log.Errorf("response status: %s", resp.Status)
|
||||
}
|
||||
|
||||
runtime.SetFinalizer(resp, func(r *http.Response) {
|
||||
@@ -174,33 +210,37 @@ 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 getLookedUpTemplate(name string, dflt string) string {
|
||||
if file, err := FindClosestParentPath(filepath.Join(".jira.d", "templates", name)); err == nil {
|
||||
return readFile(file)
|
||||
}
|
||||
if _, err := os.Stat(fmt.Sprintf("/etc/go-jira/templates/%s", name)); err == nil {
|
||||
file := fmt.Sprintf("/etc/go-jira/templates/%s", name)
|
||||
return readFile(file)
|
||||
}
|
||||
return dflt
|
||||
}
|
||||
|
||||
func (c *Cli) getTemplate(name string) string {
|
||||
if override, ok := c.opts["template"].(string); 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 t := getLookedUpTemplate(override, all_templates[override]); t != "" {
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
// create-bug etc are special, if we dont find it in the path
|
||||
// then just return the create template
|
||||
if strings.HasPrefix(name, "create-") {
|
||||
return getLookedUpTemplate(name, c.getTemplate("create"))
|
||||
}
|
||||
return getLookedUpTemplate(name, all_templates[name])
|
||||
}
|
||||
|
||||
type NoChangesFound struct{}
|
||||
@@ -211,23 +251,35 @@ func (f NoChangesFound) Error() string {
|
||||
|
||||
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"))
|
||||
tmpdir := filepath.Join(homedir(), ".jira.d", "tmp")
|
||||
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)
|
||||
log.Errorf("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)
|
||||
oldFileName := fh.Name()
|
||||
tmpFileName := fmt.Sprintf("%s.yml", oldFileName)
|
||||
|
||||
// close tmpfile so we can rename on windows
|
||||
fh.Close()
|
||||
|
||||
if err := os.Rename(oldFileName, tmpFileName); err != nil {
|
||||
log.Errorf("Failed to rename %s to %s: %s", oldFileName, tmpFileName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
fh, err = os.OpenFile(tmpFileName, os.O_RDWR|os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to reopen temp file file in %s: %s", tmpFileName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer fh.Close()
|
||||
defer func() {
|
||||
os.Remove(tmpFileName)
|
||||
}()
|
||||
@@ -262,11 +314,11 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
if editing {
|
||||
shell, _ := shellquote.Split(editor)
|
||||
shell = append(shell, tmpFileName)
|
||||
log.Debug("Running: %#v", shell)
|
||||
log.Debugf("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)
|
||||
log.Errorf("Failed to edit template with %s: %s", editor, err)
|
||||
if promptYN("edit again?", true) {
|
||||
continue
|
||||
}
|
||||
@@ -282,14 +334,14 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
|
||||
edited := make(map[string]interface{})
|
||||
if fh, err := ioutil.ReadFile(tmpFileName); err != nil {
|
||||
log.Error("Failed to read tmpfile %s: %s", tmpFileName, err)
|
||||
log.Errorf("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)
|
||||
log.Errorf("Failed to parse YAML: %s", err)
|
||||
if editing && promptYN("edit again?", true) {
|
||||
continue
|
||||
}
|
||||
@@ -307,7 +359,7 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
// 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")
|
||||
log.Infof("abort flag found in template, quiting")
|
||||
return fmt.Errorf("abort flag found in template, quiting")
|
||||
}
|
||||
|
||||
@@ -317,7 +369,7 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
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)
|
||||
log.Errorf("%s", err)
|
||||
if editing && promptYN("edit again?", true) {
|
||||
continue
|
||||
}
|
||||
@@ -333,7 +385,7 @@ func (c *Cli) editTemplate(template string, tmpFilePrefix string, templateData m
|
||||
}
|
||||
|
||||
if err := templateProcessor(json); err != nil {
|
||||
log.Error("%s", err)
|
||||
log.Errorf("%s", err)
|
||||
if editing && promptYN("edit again?", true) {
|
||||
continue
|
||||
}
|
||||
@@ -354,6 +406,27 @@ 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)
|
||||
if x := c.expansions(); len(x) > 0 {
|
||||
uri = fmt.Sprintf("%s?expand=%s", uri, strings.Join(x, ","))
|
||||
}
|
||||
|
||||
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
|
||||
@@ -362,7 +435,7 @@ func (c *Cli) FindIssues() (interface{}, error) {
|
||||
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)
|
||||
log.Errorf("%s", err)
|
||||
return nil, err
|
||||
} else {
|
||||
qbuff.WriteString(fmt.Sprintf(" AND project = '%s'", project))
|
||||
@@ -407,6 +480,7 @@ func (c *Cli) FindIssues() (interface{}, error) {
|
||||
"startAt": "0",
|
||||
"maxResults": c.opts["max_results"],
|
||||
"fields": fields,
|
||||
"expand": c.expansions(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -420,6 +494,10 @@ func (c *Cli) FindIssues() (interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -428,6 +506,10 @@ func (c *Cli) getOptString(optName string, dflt string) string {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -435,3 +517,12 @@ func (c *Cli) getOptBool(optName string, dflt bool) bool {
|
||||
return dflt
|
||||
}
|
||||
}
|
||||
|
||||
// expansions returns a comma-separated list of values for field expansion
|
||||
func (c *Cli) expansions() []string {
|
||||
expansions := make([]string, 0)
|
||||
if x, ok := c.opts["expand"].(string); ok {
|
||||
expansions = strings.Split(x, ",")
|
||||
}
|
||||
return expansions
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package cli
|
||||
package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.google.com/p/gopass"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
// "github.com/kr/pretty"
|
||||
@@ -16,23 +19,27 @@ func (c *Cli) CmdLogin() error {
|
||||
req, _ := http.NewRequest("GET", uri, nil)
|
||||
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.Infof("%s %s", req.Method, req.URL.String())
|
||||
if resp, err := c.makeRequest(req); err != nil {
|
||||
return err
|
||||
} else {
|
||||
out, _ := httputil.DumpResponse(resp, true)
|
||||
log.Debugf("%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
|
||||
if reason := resp.Header.Get("X-Authentication-Denied-Reason"); reason != "" {
|
||||
err := fmt.Errorf("Authenticaion Failed: %s", reason)
|
||||
log.Error("%s", err)
|
||||
log.Errorf("%s", err)
|
||||
return err
|
||||
}
|
||||
err := fmt.Errorf("Authentication Failed: Unknown Reason")
|
||||
log.Error("%s", err)
|
||||
log.Errorf("%s", err)
|
||||
return err
|
||||
|
||||
} else if resp.StatusCode == 200 {
|
||||
@@ -53,7 +60,7 @@ func (c *Cli) CmdLogin() error {
|
||||
}
|
||||
|
||||
func (c *Cli) CmdFields() error {
|
||||
log.Debug("fields called")
|
||||
log.Debugf("fields called")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/field", c.endpoint)
|
||||
data, err := responseToJson(c.get(uri))
|
||||
if err != nil {
|
||||
@@ -64,7 +71,7 @@ func (c *Cli) CmdFields() error {
|
||||
}
|
||||
|
||||
func (c *Cli) CmdList() error {
|
||||
log.Debug("list called")
|
||||
log.Debugf("list called")
|
||||
if data, err := c.FindIssues(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
@@ -73,19 +80,17 @@ func (c *Cli) CmdList() error {
|
||||
}
|
||||
|
||||
func (c *Cli) CmdView(issue string) error {
|
||||
log.Debug("view called")
|
||||
log.Debugf("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)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdEdit(issue string) error {
|
||||
log.Debug("edit called")
|
||||
log.Debugf("edit called")
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/editmeta", c.endpoint, issue)
|
||||
editmeta, err := responseToJson(c.get(uri))
|
||||
@@ -110,7 +115,8 @@ func (c *Cli) CmdEdit(issue string) error {
|
||||
issueData,
|
||||
func(json string) error {
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("Dryrun mode, skipping PUT")
|
||||
log.Debugf("PUT: %s", json)
|
||||
log.Debugf("Dryrun mode, skipping PUT")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.put(uri, json)
|
||||
@@ -120,13 +126,15 @@ 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))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From PUT")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
log.Errorf("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
},
|
||||
@@ -134,7 +142,7 @@ func (c *Cli) CmdEdit(issue string) error {
|
||||
}
|
||||
|
||||
func (c *Cli) CmdEditMeta(issue string) error {
|
||||
log.Debug("editMeta called")
|
||||
log.Debugf("editMeta called")
|
||||
c.Browse(issue)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/editmeta", c.endpoint, issue)
|
||||
data, err := responseToJson(c.get(uri))
|
||||
@@ -146,7 +154,7 @@ func (c *Cli) CmdEditMeta(issue string) error {
|
||||
}
|
||||
|
||||
func (c *Cli) CmdTransitionMeta(issue string) error {
|
||||
log.Debug("tranisionMeta called")
|
||||
log.Debugf("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))
|
||||
@@ -159,7 +167,7 @@ func (c *Cli) CmdTransitionMeta(issue string) error {
|
||||
|
||||
func (c *Cli) CmdIssueTypes() error {
|
||||
project := c.opts["project"].(string)
|
||||
log.Debug("issueTypes called")
|
||||
log.Debugf("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 {
|
||||
@@ -173,8 +181,8 @@ 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)
|
||||
log.Debugf("createMeta called")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", c.endpoint, project, url.QueryEscape(issuetype))
|
||||
data, err := responseToJson(c.get(uri))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -183,7 +191,7 @@ func (c *Cli) CmdCreateMeta() error {
|
||||
if val, ok := data.(map[string]interface{})["projects"]; ok {
|
||||
if len(val.([]interface{})) == 0 {
|
||||
err = fmt.Errorf("Project '%s' or issuetype '%s' unknown. Unable to createmeta.", project, issuetype)
|
||||
log.Error("%s", err)
|
||||
log.Errorf("%s", err)
|
||||
return err
|
||||
}
|
||||
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
|
||||
@@ -194,8 +202,18 @@ func (c *Cli) CmdCreateMeta() error {
|
||||
return runTemplate(c.getTemplate("createmeta"), data, nil)
|
||||
}
|
||||
|
||||
func (c *Cli) CmdComponents(project string) error {
|
||||
log.Debugf("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")
|
||||
log.Debugf("Transitions called")
|
||||
c.Browse(issue)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", c.endpoint, issue)
|
||||
data, err := responseToJson(c.get(uri))
|
||||
@@ -208,9 +226,9 @@ func (c *Cli) CmdTransitions(issue string) error {
|
||||
func (c *Cli) CmdCreate() error {
|
||||
project := c.opts["project"].(string)
|
||||
issuetype := c.getOptString("issuetype", "Bug")
|
||||
log.Debug("create called")
|
||||
log.Debugf("create called")
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", c.endpoint, project, issuetype)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", c.endpoint, project, url.QueryEscape(issuetype))
|
||||
data, err := responseToJson(c.get(uri))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -218,15 +236,20 @@ func (c *Cli) CmdCreate() 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 {
|
||||
err = fmt.Errorf("Project '%s' or issuetype '%s' unknown. Unable to create issue.", project, issuetype)
|
||||
log.Error("%s", err)
|
||||
log.Errorf("%s", err)
|
||||
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.Errorf("%s", err)
|
||||
return err
|
||||
}
|
||||
issueData["meta"] = val.([]interface{})[0]
|
||||
}
|
||||
}
|
||||
@@ -237,10 +260,11 @@ func (c *Cli) CmdCreate() error {
|
||||
fmt.Sprintf("create-%s-", sanitizedType),
|
||||
issueData,
|
||||
func(json string) error {
|
||||
log.Debug("JSON: %s", json)
|
||||
log.Debugf("JSON: %s", json)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue", c.endpoint)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
log.Debugf("POST: %s", json)
|
||||
log.Debugf("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.post(uri, json)
|
||||
@@ -253,17 +277,23 @@ func (c *Cli) CmdCreate() 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 {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From POST")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
log.Errorf("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
},
|
||||
@@ -272,7 +302,7 @@ func (c *Cli) CmdCreate() error {
|
||||
}
|
||||
|
||||
func (c *Cli) CmdIssueLinkTypes() error {
|
||||
log.Debug("Transitions called")
|
||||
log.Debugf("Transitions called")
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issueLinkType", c.endpoint)
|
||||
data, err := responseToJson(c.get(uri))
|
||||
if err != nil {
|
||||
@@ -282,7 +312,7 @@ func (c *Cli) CmdIssueLinkTypes() error {
|
||||
}
|
||||
|
||||
func (c *Cli) CmdBlocks(blocker string, issue string) error {
|
||||
log.Debug("blocks called")
|
||||
log.Debugf("blocks called")
|
||||
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"type": map[string]string{
|
||||
@@ -301,7 +331,8 @@ 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("Dryrun mode, skipping POST")
|
||||
log.Debugf("POST: %s", json)
|
||||
log.Debugf("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.post(uri, json)
|
||||
@@ -310,19 +341,21 @@ func (c *Cli) CmdBlocks(blocker string, 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)
|
||||
}
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From POST")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
log.Errorf("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdDups(duplicate string, issue string) error {
|
||||
log.Debug("dups called")
|
||||
log.Debugf("dups called")
|
||||
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"type": map[string]string{
|
||||
@@ -341,7 +374,8 @@ 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("Dryrun mode, skipping POST")
|
||||
log.Debugf("POST: %s", json)
|
||||
log.Debugf("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.post(uri, json)
|
||||
@@ -350,50 +384,114 @@ func (c *Cli) CmdDups(duplicate string, 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)
|
||||
}
|
||||
} else {
|
||||
logBuffer := bytes.NewBuffer(make([]byte, 0))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From POST")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
log.Errorf("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdWatch(issue string) error {
|
||||
watcher := c.getOptString("watcher", c.opts["user"].(string))
|
||||
log.Debug("watch called")
|
||||
func (c *Cli) CmdWatch(issue string, watcher string, remove bool) error {
|
||||
log.Debugf("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)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
if !remove {
|
||||
log.Debugf("POST: %s", json)
|
||||
log.Debugf("Dryrun mode, skipping POST")
|
||||
} else {
|
||||
log.Debugf("DELETE: %s", watcher)
|
||||
log.Debugf("Dryrun mode, skipping POST")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
resp, err := c.post(uri, json)
|
||||
|
||||
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")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
if !remove {
|
||||
err = fmt.Errorf("Unexpected Response From POST")
|
||||
} else {
|
||||
err = fmt.Errorf("Unexpected Response From DELETE")
|
||||
}
|
||||
log.Errorf("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdVote(issue string, up bool) error {
|
||||
log.Debugf("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.Debugf("POST: %s", "")
|
||||
log.Debugf("Dryrun mode, skipping POST")
|
||||
} else {
|
||||
log.Debugf("DELETE: %s", "")
|
||||
log.Debugf("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.Errorf("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdTransition(issue string, trans string) error {
|
||||
log.Debug("transition called")
|
||||
log.Debugf("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 {
|
||||
@@ -408,7 +506,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{})
|
||||
@@ -416,16 +514,17 @@ func (c *Cli) CmdTransition(issue string, trans string) error {
|
||||
}
|
||||
if transId == "" {
|
||||
err := fmt.Errorf("Invalid Transition '%s', Available: %s", trans, strings.Join(found, ", "))
|
||||
log.Error("%s", err)
|
||||
log.Errorf("%s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
handlePost := func(json string) error {
|
||||
log.Debug("POST: %s", json)
|
||||
log.Debugf("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("Dryrun mode, skipping POST")
|
||||
log.Debugf("POST: %s", json)
|
||||
log.Debugf("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.post(uri, json)
|
||||
@@ -434,12 +533,14 @@ func (c *Cli) CmdTransition(issue string, trans string) error {
|
||||
}
|
||||
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")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
log.Errorf("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -468,13 +569,14 @@ func (c *Cli) CmdTransition(issue string, trans string) error {
|
||||
}
|
||||
|
||||
func (c *Cli) CmdComment(issue string) error {
|
||||
log.Debug("comment called")
|
||||
log.Debugf("comment called")
|
||||
|
||||
handlePost := func(json string) error {
|
||||
log.Debug("JSON: %s", json)
|
||||
log.Debugf("JSON: %s", json)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/comment", c.endpoint, issue)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debug("Dryrun mode, skipping POST")
|
||||
log.Debugf("POST: %s", json)
|
||||
log.Debugf("Dryrun mode, skipping POST")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.post(uri, json)
|
||||
@@ -484,13 +586,15 @@ 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))
|
||||
resp.Write(logBuffer)
|
||||
err := fmt.Errorf("Unexpected Response From POST")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
log.Errorf("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -514,8 +618,116 @@ 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.Debugf("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.Debugf("POST: %s", json)
|
||||
log.Debugf("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.Errorf("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdLabels(action string, issue string, labels []string) error {
|
||||
log.Debugf("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.Debugf("JSON: %s", json)
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
|
||||
if c.getOptBool("dryrun", false) {
|
||||
log.Debugf("PUT: %s", json)
|
||||
log.Debugf("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.Errorf("%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")
|
||||
log.Debugf("assign called")
|
||||
|
||||
json, err := jsonEncode(map[string]interface{}{
|
||||
"name": user,
|
||||
@@ -526,7 +738,8 @@ 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("Dryrun mode, skipping PUT")
|
||||
log.Debugf("PUT: %s", json)
|
||||
log.Debugf("Dryrun mode, skipping PUT")
|
||||
return nil
|
||||
}
|
||||
resp, err := c.put(uri, json)
|
||||
@@ -535,12 +748,14 @@ func (c *Cli) CmdAssign(issue string, user string) error {
|
||||
}
|
||||
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 PUT")
|
||||
log.Error("%s:\n%s", err, logBuffer)
|
||||
log.Errorf("%s:\n%s", err, logBuffer)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -562,13 +777,35 @@ func (c *Cli) CmdExportTemplates() error {
|
||||
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)
|
||||
log.Errorf("Failed to open %s for writing: %s", templateFile, err)
|
||||
return err
|
||||
} else {
|
||||
defer fh.Close()
|
||||
log.Notice("Creating %s", templateFile)
|
||||
log.Noticef("Creating %s", templateFile)
|
||||
fh.Write([]byte(template))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cli) CmdRequest(uri, content string) (err error) {
|
||||
log.Debugf("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)
|
||||
}
|
||||
+149
-36
@@ -3,18 +3,20 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/Netflix-Skunkworks/go-jira/jira/cli"
|
||||
"github.com/Netflix-Skunkworks/go-jira"
|
||||
"github.com/coryb/optigo"
|
||||
"github.com/op/go-logging"
|
||||
"gopkg.in/coryb/yaml.v2"
|
||||
"gopkg.in/op/go-logging.v1"
|
||||
"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}"
|
||||
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)
|
||||
@@ -28,7 +30,7 @@ func main() {
|
||||
|
||||
user := os.Getenv("USER")
|
||||
home := os.Getenv("HOME")
|
||||
defaultQueryFields := "summary,created,priority,status,reporter,assignee"
|
||||
defaultQueryFields := "summary,created,updated,priority,status,reporter,assignee"
|
||||
defaultSort := "priority asc, created"
|
||||
defaultMaxResults := 500
|
||||
|
||||
@@ -54,7 +56,8 @@ Usage:
|
||||
jira create [--noedit] [-p PROJECT] <Create Options>
|
||||
jira DUPLICATE dups ISSUE
|
||||
jira BLOCKER blocks ISSUE
|
||||
jira watch ISSUE [-w WATCHER]
|
||||
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>
|
||||
@@ -62,28 +65,37 @@ Usage:
|
||||
jira reopen ISSUE [--edit] <Edit Options>
|
||||
jira start ISSUE [--edit] <Edit Options>
|
||||
jira stop ISSUE [--edit] <Edit Options>
|
||||
jira todo ISSUE [--edit] <Edit Options>
|
||||
jira done ISSUE [--edit] <Edit Options>
|
||||
jira prog|progress|in-progress [--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
|
||||
@@ -131,7 +143,16 @@ Command Options:
|
||||
"reopen": "reopen",
|
||||
"start": "start",
|
||||
"stop": "stop",
|
||||
"todo": "todo",
|
||||
"done": "done",
|
||||
"prog": "in-progress",
|
||||
"progress": "in-progress",
|
||||
"in-progress": "in-progress",
|
||||
"comment": "comment",
|
||||
"label": "labels",
|
||||
"labels": "labels",
|
||||
"component": "component",
|
||||
"components": "components",
|
||||
"take": "take",
|
||||
"assign": "assign",
|
||||
"give": "assign",
|
||||
@@ -145,6 +166,9 @@ Command Options:
|
||||
"export-templates": "export-templates",
|
||||
"browse": "browse",
|
||||
"login": "login",
|
||||
"req": "request",
|
||||
"request": "request",
|
||||
"vote": "vote",
|
||||
}
|
||||
|
||||
defaults := map[string]interface{}{
|
||||
@@ -153,17 +177,21 @@ Command Options:
|
||||
"directory": fmt.Sprintf("%s/.jira.d/templates", home),
|
||||
"sort": defaultSort,
|
||||
"max_results": defaultMaxResults,
|
||||
"method": "GET",
|
||||
"quiet": false,
|
||||
}
|
||||
opts := make(map[string]interface{})
|
||||
|
||||
overrides := make(map[string]string)
|
||||
|
||||
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, "")
|
||||
},
|
||||
@@ -172,6 +200,7 @@ Command Options:
|
||||
"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,
|
||||
@@ -179,23 +208,28 @@ Command Options:
|
||||
"a|assignee=s": setopt,
|
||||
"i|issuetype=s": setopt,
|
||||
"w|watcher=s": setopt,
|
||||
"remove": setopt,
|
||||
"r|reporter=s": setopt,
|
||||
"f|queryfields=s": setopt,
|
||||
"x|expand=s": setopt,
|
||||
"s|sort=s": setopt,
|
||||
"l|limit|max_results=i": setopt,
|
||||
"o|override=s%": &overrides,
|
||||
"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)
|
||||
log.Errorf("%s", err)
|
||||
usage(false)
|
||||
}
|
||||
args := op.Args
|
||||
opts["overrides"] = overrides
|
||||
|
||||
var command string
|
||||
if len(args) > 0 {
|
||||
@@ -204,6 +238,7 @@ Command Options:
|
||||
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:]...)
|
||||
@@ -211,7 +246,7 @@ Command Options:
|
||||
}
|
||||
}
|
||||
|
||||
if command == "" {
|
||||
if command == "" && len(args) > 0 {
|
||||
command = args[0]
|
||||
args = args[1:]
|
||||
}
|
||||
@@ -222,49 +257,58 @@ Command Options:
|
||||
// check to see if it was set in the configs:
|
||||
if value, ok := opts["command"].(string); ok {
|
||||
command = value
|
||||
} else {
|
||||
args = append([]string{command}, args...)
|
||||
} 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)
|
||||
log.Debugf("Setting %q to %#v from defaults", k, v)
|
||||
opts[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("opts: %v", opts)
|
||||
log.Debug("args: %v", args)
|
||||
log.Debugf("opts: %v", opts)
|
||||
log.Debugf("args: %v", args)
|
||||
|
||||
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")
|
||||
log.Errorf("endpoint option required. Either use --endpoint or set a endpoint option in your ~/.jira.d/config.yml file")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c := cli.New(opts)
|
||||
c := jira.New(opts)
|
||||
|
||||
log.Debug("opts: %s", opts)
|
||||
log.Debugf("opts: %s", opts)
|
||||
|
||||
setEditing := func(dflt bool) {
|
||||
log.Debug("Default Editing: %t", dflt)
|
||||
log.Debugf("Default Editing: %t", dflt)
|
||||
if dflt {
|
||||
if val, ok := opts["noedit"].(bool); ok && val {
|
||||
log.Debug("Setting edit = false")
|
||||
log.Debugf("Setting edit = false")
|
||||
opts["edit"] = false
|
||||
} else {
|
||||
log.Debug("Setting edit = true")
|
||||
log.Debugf("Setting edit = true")
|
||||
opts["edit"] = true
|
||||
}
|
||||
} else {
|
||||
if _, ok := opts["edit"].(bool); !ok {
|
||||
log.Debug("Setting edit = %t", dflt)
|
||||
log.Debugf("Setting edit = %t", dflt)
|
||||
opts["edit"] = dflt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requireArgs := func(count int) {
|
||||
if len(args) < count {
|
||||
log.Errorf("Not enough arguments. %d required, %d provided", count, len(args))
|
||||
usage(false)
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
switch command {
|
||||
case "login":
|
||||
@@ -284,7 +328,7 @@ Command Options:
|
||||
for _, issue := range issues {
|
||||
if err = c.CmdEdit(issue.(map[string]interface{})["key"].(string)); err != nil {
|
||||
switch err.(type) {
|
||||
case cli.NoChangesFound:
|
||||
case jira.NoChangesFound:
|
||||
log.Warning("No Changes found: %s", err)
|
||||
err = nil
|
||||
continue
|
||||
@@ -295,8 +339,10 @@ Command Options:
|
||||
}
|
||||
}
|
||||
case "editmeta":
|
||||
requireArgs(1)
|
||||
err = c.CmdEditMeta(args[0])
|
||||
case "transmeta":
|
||||
requireArgs(1)
|
||||
err = c.CmdTransitionMeta(args[0])
|
||||
case "issuelinktypes":
|
||||
err = c.CmdIssueLinkTypes()
|
||||
@@ -308,58 +354,125 @@ Command Options:
|
||||
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":
|
||||
err = c.CmdWatch(args[0])
|
||||
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[0], args[1])
|
||||
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 "todo":
|
||||
requireArgs(1)
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args[0], "To Do")
|
||||
case "done":
|
||||
requireArgs(1)
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args[0], "Done")
|
||||
case "in-progress":
|
||||
requireArgs(1)
|
||||
setEditing(false)
|
||||
err = c.CmdTransition(args[0], "In Progress")
|
||||
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-tempaltes":
|
||||
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)
|
||||
log.Errorf("Unknown command %s", command)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
log.Errorf("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
@@ -367,7 +480,7 @@ Command Options:
|
||||
|
||||
func parseYaml(file string, opts map[string]interface{}) {
|
||||
if fh, err := ioutil.ReadFile(file); err == nil {
|
||||
log.Debug("Found Config file: %s", file)
|
||||
log.Debugf("Found Config file: %s", file)
|
||||
yaml.Unmarshal(fh, &opts)
|
||||
}
|
||||
}
|
||||
@@ -394,9 +507,9 @@ func populateEnv(opts map[string]interface{}) {
|
||||
|
||||
func loadConfigs(opts map[string]interface{}) {
|
||||
populateEnv(opts)
|
||||
paths := cli.FindParentPaths(".jira.d/config.yml")
|
||||
paths := jira.FindParentPaths(".jira.d/config.yml")
|
||||
// prepend
|
||||
paths = append([]string{"/etc/jira-cli.yml"}, paths...)
|
||||
paths = append([]string{"/etc/go-jira.yml"}, paths...)
|
||||
|
||||
// iterate paths in reverse
|
||||
for i := len(paths) - 1; i >= 0; i-- {
|
||||
@@ -407,21 +520,21 @@ func loadConfigs(opts map[string]interface{}) {
|
||||
if stat.Mode()&0111 == 0 {
|
||||
parseYaml(file, tmp)
|
||||
} else {
|
||||
log.Debug("Found Executable Config file: %s", file)
|
||||
log.Debugf("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)
|
||||
log.Errorf("%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)
|
||||
log.Debugf("Setting %q to %#v from %s", k, v, file)
|
||||
opts[k] = v
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -63,8 +65,8 @@ fields:
|
||||
- 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}}
|
||||
# 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}}
|
||||
@@ -81,6 +83,9 @@ fields:
|
||||
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}}`
|
||||
|
||||
+77
-28
@@ -1,4 +1,4 @@
|
||||
package cli
|
||||
package jira
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -7,39 +7,51 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mgutz/ansi"
|
||||
"gopkg.in/coryb/yaml.v2"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
func homedir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return os.Getenv("USERPROFILE")
|
||||
}
|
||||
return os.Getenv("HOME")
|
||||
}
|
||||
|
||||
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)
|
||||
homedir := homedir()
|
||||
if !filepath.HasPrefix(cwd, homedir) {
|
||||
path := filepath.Join(homedir, fileName)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
path := filepath.Join(cwd, fileName)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
for true {
|
||||
cwd = filepath.Dir(cwd)
|
||||
path := filepath.Join(cwd, fileName)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
file := fmt.Sprintf("%s/%s", dir, fileName)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
paths = append(paths, file)
|
||||
if cwd[len(cwd)-1] == filepath.Separator {
|
||||
break
|
||||
}
|
||||
}
|
||||
return paths
|
||||
@@ -56,8 +68,9 @@ func FindClosestParentPath(fileName string) (string, error) {
|
||||
func readFile(file string) string {
|
||||
var bytes []byte
|
||||
var err error
|
||||
log.Debugf("readFile: reading %q", file)
|
||||
if bytes, err = ioutil.ReadFile(file); err != nil {
|
||||
log.Error("Failed to read file %s: %s", file, err)
|
||||
log.Errorf("Failed to read file %s: %s", file, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return string(bytes)
|
||||
@@ -100,8 +113,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
|
||||
}
|
||||
@@ -152,6 +176,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
|
||||
@@ -171,13 +202,16 @@ 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)
|
||||
log.Errorf("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)
|
||||
log.Errorf("Failed to execute template: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -193,7 +227,7 @@ func responseToJson(resp *http.Response, err error) (interface{}, error) {
|
||||
if resp.StatusCode == 400 {
|
||||
if val, ok := data.(map[string]interface{})["errorMessages"]; ok {
|
||||
for _, errMsg := range val.([]interface{}) {
|
||||
log.Error("%s", errMsg)
|
||||
log.Errorf("%s", errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,7 +240,7 @@ func jsonDecode(io io.Reader) interface{} {
|
||||
var data interface{}
|
||||
err = json.Unmarshal(content, &data)
|
||||
if err != nil {
|
||||
log.Error("JSON Parse Error: %s from %s", err, content)
|
||||
log.Errorf("JSON Parse Error: %s from %s", err, content)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -217,7 +251,7 @@ func jsonEncode(data interface{}) (string, error) {
|
||||
|
||||
err := enc.Encode(data)
|
||||
if err != nil {
|
||||
log.Error("Failed to encode data %s: %s", data, err)
|
||||
log.Errorf("Failed to encode data %s: %s", data, err)
|
||||
return "", err
|
||||
}
|
||||
return buffer.String(), nil
|
||||
@@ -227,13 +261,28 @@ 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)
|
||||
log.Errorf("Failed to open %s: %s", file, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
enc := json.NewEncoder(fh)
|
||||
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.Errorf("Failed to open %s: %s", file, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if out, err := yaml.Marshal(data); err != nil {
|
||||
log.Errorf("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 {
|
||||
@@ -269,7 +318,7 @@ func yamlFixup(data interface{}) (interface{}, error) {
|
||||
}
|
||||
default:
|
||||
err := fmt.Errorf("YAML: key %s is type '%T', require 'string'", key, k)
|
||||
log.Error("%s", err)
|
||||
log.Errorf("%s", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -295,7 +344,7 @@ func yamlFixup(data interface{}) (interface{}, error) {
|
||||
}
|
||||
return copy, nil
|
||||
case string:
|
||||
if d == "" {
|
||||
if d == "" || d == "\n" {
|
||||
return nil, nil
|
||||
}
|
||||
return d, nil
|
||||
@@ -306,16 +355,16 @@ func yamlFixup(data interface{}) (interface{}, error) {
|
||||
|
||||
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)
|
||||
log.Errorf("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)
|
||||
log.Errorf("%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)
|
||||
log.Errorf("Failed to mkdir -p %s: %s", dir, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user