Compare commits

..

90 Commits

Author SHA1 Message Date
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
Cory Bennett b5417ef585 update version tag to prefix with v 2016-01-21 17:38:50 -08:00
Cory Bennett c5af781c41 Updated Changelog 2016-01-21 17:36:23 -08:00
Cory Bennett f2c4df9b3e [issue #28] check to make sure we got back issuetypes for create metadata 2016-01-21 17:35:18 -08:00
Cory Bennett 1dde7e06e6 gofmt 2016-01-21 10:52:15 -08:00
coryb 7bc1897792 Merge pull request #27 from blalor/insecure-skip-verify
Add insecure option for TLS endpoints
2016-01-21 10:15:29 -08:00
Brian Lalor 37aab3580b Add insecure option for TLS endpoints
This gives the option of disabling TLS certificate verification for
the server.

Closes #25
2016-01-21 12:30:23 -05: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
coryb 42990d8ca0 Merge pull request #21 from mikepea/label_command
Add 'labels' command to set/add/remove labels
2016-01-04 08:50:15 -08:00
Mike Pountney 8e662462da Correct naming of parameter: set/add/remove are actions.
'command' is not approprirate for the set/add/remove operations, and is
confusing. Rename to 'action' to remove this confusion.
2016-01-03 20:28:02 -08:00
Mike Pountney ad7bb2b724 Tweak CmdLabels args so that magic happens with CLI
The CLI looks for commands (like 'labels') in the first two positional args. This
is how commands like 'BLOCKER blocks ISSUE' work.

As per @coryb's comments in #21 - this allows us to support set/add/remove in a
consistent way for other types: components for example.

This commit rearranges the command to be as follows:

    jira (set/add/remove) labels ISSUE LABELS...

The options to CmdLabels have been reordered accordingly.
2016-01-03 20:10:49 -08:00
Mike Pountney a8cce44178 Merge branch 'master' into label_command 2016-01-03 19:50:47 -08:00
coryb 35955a7a93 Merge pull request #22 from mikepea/library_break_out
Expose key functionality for library consumption
2015-12-31 12:22:09 -08:00
Mike Pountney f349e25bb9 Expose ViewTicket as per FindIssues
This allows external users of the API to retreive issue details, in the same
manner that FindIssues provides for lists of issues.
2015-12-31 12:06:04 -08:00
Mike Pountney a92a93b282 Add exposed versions of getTemplate and runTemplate
GetTemplate and RunTemplate allow external users to access the template system,
and in particular allow them to provide their own Buffer to write the output to.

I've implemented exposed versions calling the private functions, rather than breaking
the internal API. If this isn't a concern, we should remove getTemplate and runTemplate
in a future commit.
2015-12-31 12:03:22 -08:00
Mike Pountney 8645ef11f1 go fmt 2015-12-31 11:52:55 -08:00
Mike Pountney e042a3e62a Add 'labels' command to set/add/remove labels
This adds 'labels' command, which allows for the setting, addition and removal
of labels on an issue.

'set' action resets the issue labels to the list provided.
'add' action adds the supplied labels to the issue
'remove' action removes the supplied labels from the issue

The API already gracefully handles duplication, removal of non-existant labels, and the
supplying of an empty list of labels (which is useful in the case of 'set')

Eg

jira labels TEST-123 add label1 label2 label3
jira labels TEST-123 remove label1 label2
jira labels TEST-123 set label1 label2 label3
jira labels TEST-123 set # clears any labels on the issue
jira labels TEST-123 add # no-op
jira labels TEST-123 remove # no-op

This mirrors the functionality of the API.
2015-12-24 11:30:52 -08:00
coryb a738d1515e Merge pull request #20 from mikepea/add_join_template_func
Add a 'join' func to the template engine
2015-12-23 12:05:34 -08:00
Mike Pountney d4f15ae5c6 Add a 'join' func to the template engine
I needed this so that I can display JIRA labels in my 'list' template.
2015-12-23 06:15:31 -08:00
Cory Bennett bc70b43868 make "jira" golang package, move code from jira/cli to root, move jira/main.go to main/main.go 2015-12-22 17:56:53 -08:00
Cory Bennett e24b431b7a Updated Changelog 2015-12-09 20:58:11 -08:00
Cory Bennett 101bc1da68 fix jira trans TRANS ISSUE (case sensitivity issue), also go fmt 2015-12-09 20:57:10 -08:00
Cory Bennett 63e035c5c1 Updated Changelog 2015-12-03 14:47:55 -08:00
Cory Bennett 40bafc9b66 need to default "quiet" to false 2015-12-03 14:47:31 -08:00
Cory Bennett 5d863ffed4 Updated Changelog 2015-12-03 12:56:18 -08:00
Cory Bennett 577394b0bd add --quiet command to not print the OK ..
add --saveFile option to print the issue/link to a file on create command
2015-12-03 12:55:14 -08:00
Cory Bennett c1a7e1bbdb fix overrides 2015-12-03 12:21:55 -08:00
Cory Bennett f904f3c089 add abstract request wrapper to allow you to access/process random apis
supported by Jira but not yet supported by go-jira
2015-12-03 11:35:51 -08:00
Cory Bennett e35e518368 Updated Changelog 2015-11-23 17:09:17 -08:00
Cory Bennett 159d142f37 jira edit should not require one arguemnt (allow for --query) 2015-11-23 17:08:40 -08:00
Cory Bennett df84d47552 fix CURVER in case of building from src tar.gz 2015-11-23 14:35:41 -08:00
Cory Bennett fa4ce5647d tweak build 2015-11-23 14:23:29 -08:00
Cory Bennett 713d300a57 Updated Changelog 2015-11-23 14:17:41 -08:00
Cory Bennett c8ae7fc685 [#17] print usage on missing arguments 2015-11-23 14:17:09 -08:00
Cory Bennett 80322b648e bump version 2015-11-17 12:30:11 -08:00
Cory Bennett f2076a0977 Updated Changelog 2015-11-17 12:30:11 -08:00
coryb 886adb5db2 Merge pull request #16 from oschrenk/fix-typo
s/enpoint/endpoint/g
2015-10-16 08:50:52 -07:00
Oliver Schrenk 3bdbdbdaff s/enpoint/endpoint/g 2015-10-16 11:12:25 +02:00
coryb 544b923fab Merge pull request #14 from mikepea/ls_with_updated
Add 'updated' to queryfields; dateFormat template command; bug fix.
2015-10-15 08:37:47 -07:00
Mike Pountney 13a69e6f44 Implement dateFormat template command
Wrapper around time.Format, so that we can make concise lists without
the whole JIRA ISO timestamp, eg:

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