Compare commits

..

41 Commits

Author SHA1 Message Date
Cory Bennett adc08935b4 Updated Changelog 2016-07-30 20:13:00 -07:00
Cory Bennett 073c8a3694 [#43] add support for jira done|todo|prog commands 2016-07-30 20:05:13 -07:00
Cory Bennett c4a31a498e [issue #46] add documentation for how to create/edit templates 2016-07-08 12:12:02 -07:00
coryb bcad37089a Merge pull request #24 from mikepea/edit_template_common
Reporter is not generally editable.
2016-06-30 08:53:34 -07:00
Cory Bennett b2ba8de15d Updated Changelog 2016-06-29 23:11:04 -07:00
Cory Bennett 6016bda571 [#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. 2016-06-29 23:09:27 -07:00
Cory Bennett 34ca09cf1a trim out unused platforms, we can add then back in on request
publish windows binaries as .exe
2016-06-29 23:08:54 -07:00
Cory Bennett d7fb88ee41 fix for versions with 'v' prefix 2016-06-28 23:07:06 -07:00
Cory Bennett de4fe76fec fix v in version prefix 2016-06-28 23:02:03 -07:00
Cory Bennett 5b870cb7a2 Updated Changelog 2016-06-28 23:00:49 -07:00
Cory Bennett 89bb82b3f2 use USERPROFILE instead of HOME for windows, rework paths to use
filepath.Join for better cross platform support
2016-06-29 06:57:59 +01:00
Cory Bennett dd0f5efd32 Merge branch 'master' of github.com:Netflix-Skunkworks/go-jira 2016-06-29 01:40:21 +01:00
Cory Bennett 68b5e60dd9 make binary name .exe for windows 2016-06-29 01:26:28 +01:00
Cory Bennett 71acc5d7fc update Makefile so it builds under cygwin on windows 2016-06-27 15:51:21 -07:00
Cory Bennett 4f91cecf25 fix build under Cygwin on Windows 2016-06-27 15:46:14 -07:00
coryb 688b987895 Merge pull request #39 from mikepea/system_template_dir
Include templates from a system path
2016-03-30 14:47:17 -07:00
Mike Pountney 71bb04fabb Include templates from a system path
I've found that several of the built-in templates need a bit of work
to be useful with our JIRA installation (eg #24), so this allows for admins
to make a site-specific set of defaults easily.
2016-03-30 20:20:55 +01:00
coryb 3a9f763f9d Merge pull request #38 from jglick/patch-1
Noting jira login
2016-03-30 08:48:21 -07:00
Jesse Glick d86d85f7b2 Noting jira login 2016-03-30 09:39:49 -04:00
coryb 4b798cbfb4 Merge pull request #37 from tobyjoe/add-resource-expansion
Added support for the ```expand``` option for Issues
2016-02-22 09:04:54 -08:00
tobyjoe 598924b51d Added support for the ``expand`` option for Issues
The ```expand``` option is used to specify resource expansion in the
Jira REST API.

It's particularly useful for things like fetching the ```changelog``` of
an Issue.

This PR adds the support to the ```ListIssues``` and ```ViewIssue```
functions of the ```jira.Cli``` struct.

I'm happy to add tests, but there is currently no test suite in the
master branch, so I did not want to bog down the PR with tangential features.
2016-02-22 08:46:08 -05:00
Cory Bennett 674957af5d change for api changes to go-logging 2016-02-11 15:11:06 -08:00
coryb c568d7e921 fix #36, build instructions needed updating 2016-02-10 17:03:27 -08:00
coryb 6eb3567ca5 Merge pull request #35 from QuinnyPig/fix-readme
Fix path for installation instructions
2016-02-02 12:06:48 -08:00
Corey Quinn 87ec73c5c3 Fix path for command 2016-02-02 11:19:54 -08:00
coryb 23551abb11 Merge pull request #34 from jonathanio/fix/issuetypes-url-escaping
Fix issuetype calls adding URL escaping
2016-02-01 08:46:21 -08:00
Jonathan Wright 693e1441f7 Fix issuetype calls adding URL escaping
It is valid within JIRA to have issue types with spaces (for example we use
"Pro-Active Task") however the issuetype variable is not escaped prior to
formatting into the uri string.

This fix imports "net/url" and escapes all inclusions of issuetype into uri.
The only other variables formatted into uri are c.endpoint and issue which
shouldn't contain special characters.
2016-02-01 11:10:30 +00:00
Cory Bennett 6e5cc9821e Updated Changelog 2016-01-29 09:04:49 -08:00
Cory Bennett 9e90376816 Merge branch 'master' of github.com:Netflix-Skunkworks/go-jira 2016-01-29 09:02:39 -08:00
coryb 20b32c2ed6 Merge pull request #33 from mikepea/make_jirad
Fixes #32 - make path to cookieFile if it's not present
2016-01-29 09:01:29 -08:00
Mike Pountney ac170e9ab1 Fixes #32 - make path to cookieFile if it's not present
TL;DR, this ensures ~/jira.d is present, with 0755 perms.

If ~/jira.d isn't present, we can't write to the cookieFile, which
breaks CmdLogin. This is particularly an issue when using /etc/go-jira.yml
to get an entire team using go-jira easily :)

This fixes this by ensuring the cookieFile dir is present before
writing to it.
2016-01-29 11:12:25 +00:00
coryb d8bce08d3a Merge pull request #31 from mikepea/component_mgmt
Add component/components support: add and list for now.
2016-01-28 16:46:06 -08:00
Cory Bennett 382bf4faeb Merge branch 'master' of github.com:Netflix-Skunkworks/go-jira 2016-01-28 16:42:08 -08:00
Mike Pountney 595a5212b4 Add component/components support: add and list for now.
This adds the basics of the component API:

* listing of components assigned to a project
* adding new components to a project.

The interface to 'add component' is similar to that of 'labels', as
previously discussed. We can implement remove/update later.
2016-01-29 00:22:26 +00:00
coryb f595801202 Merge pull request #30 from mikepea/unwatch_support
Support for removing a given watcher
2016-01-23 22:54:05 -08:00
coryb 404caf6400 Merge pull request #26 from mikepea/vote_support
Add 'vote' and 'unvote'
2016-01-23 22:52:31 -08:00
Mike Pountney f7eb04e36d Tweak the CmdWatch contract and add watcher remove support
This adjusts the CmdWatch interface as per discussion in
https://github.com/Netflix-Skunkworks/go-jira/pull/26

It also exposes public versions of the c.getOptString and c.getOptBool
utility functions, again as discussed.

The interface to CmdWatch now includes the user to be watched (rather than
depending on the opt[] map. This makes CmdWatch more useful externally.

A '--remove' option has been created, to allow for removal of a given watcher.
This was deliberately not included in the defaults map, as it is specifically only
used for 'watch' command right now. It should be moved up to a default if it becomes
a more common option, I guess (as 'remove is false' isn't a bad default)
2016-01-24 03:00:39 +00:00
Mike Pountney b0d4f7273d Amend vote/unvote to be vote/vote --down
This simplifies the interface to voting, as per the discussion in
https://github.com/Netflix-Skunkworks/go-jira/pull/26

Basically, DRY out the logic into a single CmdVote function based
around an 'up' bool. Similarly, make a single CLI command with an
option to do the downvote.
2016-01-24 02:23:08 +00:00
Mike Pountney a927181db1 Merge branch 'master' into vote_support 2016-01-23 18:06:16 +00:00
Mike Pountney ff56136937 Add 'vote' and 'unvote'
This adds support for voting on issues via CmdVote() and CmdUnvote()

Voting on issues is always done as the logged in user, it appears you
can't case a vote for another user:

https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addVote

This required adding a cli.delete() handler, naturally with no content
(as per RFC2616)

This is ripe for DRY-ing out, but I will leave that for a future PR.

Worth noting is that you cannot vote for your own issues, this results in:

    2016-01-13T21:35:41.315Z ERROR [cli.go:184] response status: 404 Not Found
    2016-01-13T21:35:41.315Z ERROR [commands.go:439] Unexpected Response From POST:
    {snip}
    {"errorMessages":["You cannot vote for an issue you have reported."],"errors":{}}
2016-01-13 21:41:34 +00:00
Mike Pountney e58625b00c Reporter is not generally editable.
Support the lowest common denominator for default_edit_template, as per #23

`reporter` is not usually editable by unprivileged users. Even if it is left as-is,
the PUT request is trying to update it, resulting in the following error:

    {"errorMessages":[],"errors":{"reporter":"Field 'reporter' cannot be set. It is not on the appropriate screen, or unknown."}}

Commenting out the field leaves the information visible, so is a reasonable compromise.
2016-01-03 22:25:05 -08:00
9 changed files with 486 additions and 196 deletions
+2
View File
@@ -5,3 +5,5 @@ src/github.com/docopt/
src/github.com/mgutz/
src/github.com/op/
src/gopkg.in/
jira
jira.exe
+25
View File
@@ -1,5 +1,30 @@
# 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)]
+31 -18
View File
@@ -1,29 +1,41 @@
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
# freebsd-386 \
# freebsd-arm \
# linux-arm \
# openbsd-386 \
# openbsd-amd64 \
# darwin-386
export GOPATH=$(shell pwd)
GOBIN ?= $(shell pwd)
NAME=jira
BIN ?= $(GOBIN)/$(NAME)
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
CURVER ?= $(shell [ -d .git ] && git describe --abbrev=0 --tags || grep ^\#\# CHANGELOG.md | awk '{print $$2; exit}')
LDFLAGS:=-X jira.VERSION=$(patsubst v%,%,$(CURVER)) -w
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
@@ -34,16 +46,16 @@ else
endif
build: src/github.com/Netflix-Skunkworks/go-jira
$(GOBUILD) -o $(BIN) main/main.go
$(GOBUILD) -o '$(BIN)' main/main.go
src/%:
mkdir -p $(@D)
test -L $@ || ln -sf ../../.. $@
test -L $@ || ln -sf '$(GOPATH)' $@
go get -v $* $*/main
cross-setup:
for p in $(PLATFORMS); do \
echo "Building for $$p"; \
echo Building for $$p"; \
cd $(GOROOT)/src && sudo GOROOT_BOOTSTRAP=$(GOROOT) GOOS=$${p/-*/} GOARCH=$${p/*-/} bash ./make.bash --no-clean; \
done
@@ -54,6 +66,7 @@ all:
echo "Building for $$p"; \
${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 main/*.go *.go
@@ -65,7 +78,7 @@ 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 main/*.go *.go | grep -vE 'gofmt|go fmt'
@git log --pretty=format:"* %s [%cn] [%h]" --no-merges ^v$(CURVER) HEAD main/*.go *.go | grep -vE 'gofmt|go fmt'
update-changelog:
@echo "# Changelog" > CHANGELOG.md.new; \
@@ -81,7 +94,7 @@ update-changelog:
git tag v$(NEWVER)
version:
@echo $(patsubst v%,%,$(CURVER))
@echo $(CURVER)
clean:
rm -rf pkg dist bin src ./$(NAME)
+28 -18
View File
@@ -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,7 +215,7 @@ 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:
+99 -44
View File
@@ -6,14 +6,16 @@ import (
"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"
@@ -32,7 +34,7 @@ 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, "/"))
@@ -52,7 +54,7 @@ func New(opts map[string]interface{}) *Cli {
cli := &Cli{
endpoint: url,
opts: opts,
cookieFile: fmt.Sprintf("%s/.jira.d/cookies.js", homedir),
cookieFile: filepath.Join(homedir, ".jira.d", "cookies.js"),
ua: &http.Client{
Jar: cookieJar,
Transport: transport,
@@ -87,6 +89,7 @@ func (c *Cli) saveCookies(cookies []*http.Cookie) {
}
jsonWrite(c.cookieFile, mergedCookies)
} else {
mkdir(path.Dir(c.cookieFile))
jsonWrite(c.cookieFile, cookies)
}
}
@@ -98,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
}
@@ -118,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))
@@ -148,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 {
@@ -171,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) {
@@ -193,33 +214,33 @@ 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{}
@@ -230,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)
}()
@@ -281,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
}
@@ -301,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
}
@@ -326,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")
}
@@ -336,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
}
@@ -352,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
}
@@ -382,6 +415,10 @@ func (c *Cli) SaveData(data interface{}) error {
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
@@ -398,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))
@@ -443,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
@@ -456,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
@@ -464,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
@@ -471,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
}
+181 -67
View File
@@ -2,10 +2,12 @@ package jira
import (
"bytes"
"errors"
"fmt"
"golang.org/x/crypto/ssh/terminal"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
// "github.com/kr/pretty"
@@ -22,22 +24,22 @@ func (c *Cli) CmdLogin() error {
passwd := string(pwbytes)
req.SetBasicAuth(user, passwd)
log.Info("%s %s", req.Method, req.URL.String())
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.Debug("%s", out)
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 {
@@ -58,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 {
@@ -69,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 {
@@ -78,7 +80,7 @@ func (c *Cli) CmdList() error {
}
func (c *Cli) CmdView(issue string) error {
log.Debug("view called")
log.Debugf("view called")
c.Browse(issue)
data, err := c.ViewIssue(issue)
if err != nil {
@@ -88,7 +90,7 @@ func (c *Cli) CmdView(issue string) error {
}
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))
@@ -113,8 +115,8 @@ func (c *Cli) CmdEdit(issue string) error {
issueData,
func(json string) error {
if c.getOptBool("dryrun", false) {
log.Debug("PUT: %s", json)
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)
@@ -132,7 +134,7 @@ func (c *Cli) CmdEdit(issue string) error {
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
}
},
@@ -140,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))
@@ -152,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))
@@ -165,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 {
@@ -179,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
@@ -189,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 {
@@ -200,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))
@@ -214,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
@@ -229,13 +241,13 @@ func (c *Cli) CmdCreate() 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 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.Error("%s", err)
log.Errorf("%s", err)
return err
}
issueData["meta"] = val.([]interface{})[0]
@@ -248,11 +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("POST: %s", json)
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)
@@ -281,7 +293,7 @@ func (c *Cli) CmdCreate() error {
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
}
},
@@ -290,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 {
@@ -300,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{
@@ -319,8 +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("POST: %s", json)
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)
@@ -336,14 +348,14 @@ func (c *Cli) CmdBlocks(blocker string, issue string) error {
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{
@@ -362,8 +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("POST: %s", json)
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)
@@ -379,28 +391,40 @@ func (c *Cli) CmdDups(duplicate string, issue string) error {
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("POST: %s", json)
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
}
@@ -412,15 +436,62 @@ func (c *Cli) CmdWatch(issue string) error {
} 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 {
@@ -443,17 +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("POST: %s", json)
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)
@@ -469,7 +540,7 @@ func (c *Cli) CmdTransition(issue string, trans string) error {
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
@@ -498,14 +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("POST: %s", json)
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)
@@ -523,7 +594,7 @@ func (c *Cli) CmdComment(issue string) error {
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
}
}
@@ -547,19 +618,62 @@ 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.Debug("label called")
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.Debug("JSON: %s", json)
log.Debugf("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")
log.Debugf("PUT: %s", json)
log.Debugf("Dryrun mode, skipping POST")
return nil
}
resp, err := c.put(uri, json)
@@ -577,7 +691,7 @@ func (c *Cli) CmdLabels(action string, issue string, labels []string) error {
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
}
}
@@ -613,7 +727,7 @@ func (c *Cli) CmdLabels(action string, issue string, labels []string) error {
}
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,
@@ -624,8 +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("PUT: %s", json)
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)
@@ -641,7 +755,7 @@ func (c *Cli) CmdAssign(issue string, user string) error {
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
@@ -663,11 +777,11 @@ 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))
}
}
@@ -675,7 +789,7 @@ func (c *Cli) CmdExportTemplates() error {
}
func (c *Cli) CmdRequest(uri, content string) (err error) {
log.Debug("request called")
log.Debugf("request called")
if !strings.HasPrefix(uri, "http") {
uri = fmt.Sprintf("%s%s", c.endpoint, uri)
+75 -20
View File
@@ -5,8 +5,8 @@ import (
"fmt"
"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"
@@ -56,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>
@@ -64,6 +65,9 @@ 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
@@ -72,6 +76,8 @@ Usage:
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
@@ -137,9 +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",
@@ -155,6 +168,7 @@ Command Options:
"login": "login",
"req": "request",
"request": "request",
"vote": "vote",
}
defaults := map[string]interface{}{
@@ -194,8 +208,10 @@ 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%": &opts,
@@ -206,10 +222,11 @@ Command Options:
"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
@@ -250,36 +267,36 @@ Command Options:
// 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 endpoint 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 := 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
}
}
@@ -287,7 +304,7 @@ Command Options:
requireArgs := func(count int) {
if len(args) < count {
log.Error("Not enough arguments. %d required, %d provided", count, len(args))
log.Errorf("Not enough arguments. %d required, %d provided", count, len(args))
usage(false)
}
}
@@ -350,7 +367,9 @@ Command Options:
}
case "watch":
requireArgs(1)
err = c.CmdWatch(args[0])
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)
@@ -379,6 +398,18 @@ Command Options:
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)
@@ -389,6 +420,23 @@ Command Options:
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))
@@ -404,6 +452,13 @@ Command Options:
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 := ""
@@ -412,12 +467,12 @@ Command Options:
}
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)
@@ -425,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)
}
}
@@ -465,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
}
}
+6 -2
View File
@@ -12,6 +12,7 @@ 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,
@@ -64,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}}
@@ -82,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}}`
+39 -27
View File
@@ -12,35 +12,46 @@ import (
"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
@@ -57,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)
@@ -195,11 +207,11 @@ func runTemplate(templateContent string, data interface{}, out io.Writer) error
},
}
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
}
}
@@ -215,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)
}
}
}
@@ -228,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
}
@@ -239,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
@@ -249,7 +261,7 @@ 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)
@@ -260,11 +272,11 @@ 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)
log.Errorf("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)
log.Errorf("Failed to marshal yaml %v: %s", data, err)
os.Exit(1)
} else {
fh.Write(out)
@@ -306,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
}
}
@@ -343,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
}
}