Compare commits

..

320 Commits

Author SHA1 Message Date
Cory Bennett e21f18e987 Updated Changelog 2017-09-17 16:30:41 -07:00
Cory Bennett 96bbbd7ce3 fix table tests for issuetype addition 2017-09-17 16:15:28 -07:00
Cory Bennett 3e8b9bd9f5 need issuetype to use the default list table template now 2017-09-17 16:15:00 -07:00
Cory Bennett 8fe0d98d54 version bump 2017-09-17 15:53:46 -07:00
Cory Bennett 1a3eaf340c Updated Changelog 2017-09-17 15:53:46 -07:00
Cory Bennett 96b4658dcb fix logic for detecting changes in README.md 2017-09-17 15:53:30 -07:00
Cory Bennett c9d8dfbe55 [#102] add issuetype into the default queryfields and add it to the default table list template 2017-09-17 15:49:05 -07:00
Cory Bennett 238e16fc09 version bump 2017-09-17 15:38:25 -07:00
Cory Bennett 22a354ce42 Updated Changelog 2017-09-17 15:38:24 -07:00
Cory Bennett d9736919bb Updated Usage 2017-09-17 15:38:24 -07:00
Cory Bennett 3c16e1754a fix whitespace, --help-long always returns non-zero exit 2017-09-17 15:38:16 -07:00
Cory Bennett 650bc4b50d fix whitespace 2017-09-17 15:33:10 -07:00
Cory Bennett b1c9bf5ae5 automatically update usage in README.md 2017-09-17 15:32:24 -07:00
Cory Bennett 66eb7bff38 [#100] add support for posting, fetching, listing and removing attachments 2017-09-17 15:18:06 -07:00
Cory Bennett abc82b909e version bump 2017-09-15 00:33:12 -07:00
Cory Bennett dabf4cf034 Updated Changelog 2017-09-15 00:33:12 -07:00
Cory Bennett 79a6381307 update usage 2017-09-15 00:31:43 -07:00
Cory Bennett 893454fc69 [#87] add various commands for interacting with epics 2017-09-15 00:28:56 -07:00
Cory Bennett d5b9631cf4 make test less error prone 2017-09-13 13:38:55 -07:00
Cory Bennett e841270b83 version bump 2017-09-13 12:31:16 -07:00
Cory Bennett 2ededeeaf7 Updated Changelog 2017-09-13 12:31:16 -07:00
Cory Bennett 00cba793ad tweaks for templates in named queries to work better 2017-09-13 12:17:59 -07:00
Cory Bennett fb43753c31 [#99] add support for named queries to be stored in configs 2017-09-13 11:12:58 -07:00
Cory Bennett 5085a14494 add license badge 2017-09-13 00:15:09 -07:00
Cory Bennett 5da04c1f86 [#98] add --status option for JQL filter on status with list command 2017-09-13 00:12:20 -07:00
Cory Bennett 052e038d73 version bump 2017-09-11 00:16:10 -07:00
Cory Bennett a7f1323f34 Updated Changelog 2017-09-11 00:16:10 -07:00
Cory Bennett 8d27b736ca track *.lock 2017-09-11 00:16:01 -07:00
Cory Bennett c3c008e53d running dep prune 2017-09-11 00:11:56 -07:00
Cory Bennett 608e586d1c use --gjq for GJson Query to filter json response data, remove --jq option 2017-09-10 22:48:40 -07:00
Cory Bennett 2c552ac530 fix field tag syntax 2017-09-09 19:13:58 -07:00
Cory Bennett 1d269183c3 add --jq option to run a json query against Jira service response json 2017-09-09 19:02:23 -07:00
Cory Bennett e0e1e5b941 use {{jira}} in custom-command docs 2017-09-09 17:44:56 -07:00
Cory Bennett 941824d7f8 add '{{jira}}' template macro to refer to path of currently running jira command 2017-09-09 17:40:26 -07:00
Cory Bennett c585244f3e add basic tests for custom-commands 2017-09-09 17:20:57 -07:00
Cory Bennett 29b95a52cb version bump 2017-09-08 18:54:34 -07:00
Cory Bennett f556375242 Updated Changelog 2017-09-08 18:54:34 -07:00
Cory Bennett 86b963bdb5 update deps for kingpeon update
use os.exec instead of syscall.exec for windows
2017-09-08 18:53:21 -07:00
Cory Bennett 036ebb4bf7 make custom-commands example copy/pasteable 2017-09-07 08:39:37 -07:00
Cory Bennett 7d481fe965 version bump 2017-09-06 23:23:47 -07:00
Cory Bennett 4709bbbe38 Updated Changelog 2017-09-06 23:23:47 -07:00
Cory Bennett 1c79a80389 add COLUMNS back in for jira ls --template table testing 2017-09-06 23:07:48 -07:00
Cory Bennett 6a879959be fix perms for test .gnupg directory 2017-09-06 22:34:15 -07:00
Cory Bennett d46f9495e7 assume tests always run in jira cloud 2017-09-06 22:30:59 -07:00
Cory Bennett e6faee1573 [#66] add --started option to jira worklog add to change the start time for worklog 2017-09-06 22:30:15 -07:00
Cory Bennett c4be59cae3 [#45] automatically add comment to issue even if transition does not support comment updates during transtion 2017-09-06 21:28:57 -07:00
Cory Bennett 9cc91f7108 use xgo tool rather than docker image directly 2017-09-06 12:29:06 -07:00
Cory Bennett da8ee59ebb version bump 2017-09-06 12:07:51 -07:00
Cory Bennett 4386b9c541 Updated Changelog 2017-09-06 12:06:37 -07:00
Cory Bennett aa876cd588 update dependencies 2017-09-06 11:35:00 -07:00
Cory Bennett 9453179251 update for github.com/AlecAivazis/survey => gopkg.in/AlecAivazis/survey.v1 package 2017-09-06 08:40:34 -07:00
Cory Bennett 4d79af4f5e use stdout to determin output terminal size 2017-09-06 00:33:53 -07:00
Cory Bennett 2cb6bbb10d Updated Changelog 2017-09-06 00:06:31 -07:00
Cory Bennett 1106558703 [#13] change default input syntax to not require escaping for special characters 2017-09-06 00:04:54 -07:00
Cory Bennett 6e027657c2 Updated Changelog 2017-09-05 23:47:40 -07:00
Cory Bennett d87ca7a55d remove cruft 2017-09-05 23:46:30 -07:00
Cory Bennett 1b854da23b fix build for windows 2017-09-05 23:45:29 -07:00
Cory Bennett 6719e926f0 Merge branch 'v1' 2017-09-05 23:26:01 -07:00
Cory Bennett 41e0adcf27 formatting 2017-09-05 23:03:34 -07:00
Cory Bennett d9fe99041d mention golang import 2017-09-05 23:01:37 -07:00
Cory Bennett f1b8c64e33 change the default log output format 2017-09-05 22:58:30 -07:00
Cory Bennett 808e370297 update docs 2017-09-05 16:59:03 -07:00
Cory Bennett 49fbc4fd81 update for peon change 2017-09-05 16:07:50 -07:00
Cory Bennett 49f6cdc54e tweak auto-login so it does not print the standard jira login command output 2017-09-04 19:09:38 -07:00
Cory Bennett c226077320 add --quiet global option 2017-09-04 18:12:32 -07:00
Cory Bennett c0358eb67c refactor to allow for --insecure and --unixproxy arguments 2017-09-04 17:53:01 -07:00
Cory Bennett 21920c5888 handle html response on expired cookies (require X-Ausername header to always be present) 2017-09-03 23:28:17 -07:00
Cory Bennett 7ab6c22751 allow login prompt to be interrupted 2017-09-03 18:57:08 -07:00
Cory Bennett bccf09f83a fmt -> log typo 2017-09-03 01:02:16 -07:00
Cory Bennett e72479ca2a make ~/.jira.d directory if not already present 2017-09-03 00:58:56 -07:00
Cory Bennett e5dd3a7acf use go 1.8 2017-09-03 00:54:50 -07:00
Cory Bennett e04b506c58 fix go vet 2017-09-03 00:52:11 -07:00
Cory Bennett ba35f55a48 fix tests 2017-09-03 00:40:43 -07:00
Cory Bennett dc021818bb add OK printf 2017-09-03 00:40:08 -07:00
Cory Bennett b120c0ba10 change --method to use -M for backwards compat 2017-09-03 00:39:28 -07:00
Cory Bennett 2638396606 add resolution to dup'd issues when necessary 2017-09-03 00:38:37 -07:00
Cory Bennett ad1a62a88d call correct function for labels remove|set commands 2017-09-02 19:54:18 -07:00
Cory Bennett 4b60313d32 data argument is optional (for GET and DELETE requests) 2017-09-02 19:53:51 -07:00
Cory Bennett 84119a2111 fix usage, overrides not serialized correctly 2017-09-02 19:53:25 -07:00
Cory Bennett fa4ac258d0 fix jira ISSUE-123 command line parsing 2017-09-02 18:04:53 -07:00
Cory Bennett aed952b59e add logger object to jiracmd 2017-09-02 14:09:49 -07:00
Cory Bennett 979da1f3a5 refactor for GlobalOptions and CommonOptions 2017-09-02 14:05:27 -07:00
Cory Bennett 65891e7b3b update for testing 2017-09-02 14:04:49 -07:00
Cory Bennett 0a5510b231 move commands from jiracli package to jiracmd package 2017-09-02 09:14:54 -07:00
Cory Bennett de1460ddc9 use cliError for all cli errors 2017-09-01 23:32:40 -07:00
Cory Bennett fb1bfeb8f5 use jiracli.Error object to disambiguate between kingpin errors and cli errors 2017-09-01 23:30:59 -07:00
Cory Bennett fce78dea68 use correct variables 2017-09-01 23:29:54 -07:00
Cory Bennett f54e619194 fix OK output 2017-09-01 23:29:47 -07:00
Cory Bennett f38a0b888a udpate oreo for request replays 2017-08-31 22:58:55 -07:00
Cory Bennett 560998253d fix bogus logic error 2017-08-31 22:58:26 -07:00
Cory Bennett 36c26c523a fix stray newline for list table template 2017-08-31 22:57:57 -07:00
Cory Bennett 0f12ba6ba5 add --saveFile for create 2017-08-31 22:57:31 -07:00
Cory Bennett af45280d9e remove accidentally commited binary 2017-08-31 16:15:21 -07:00
Cory Bennett 3942f6f5d6 fix dynamic table output when not on tty 2017-08-31 16:00:45 -07:00
Cory Bennett da9a2b2b90 when using --verbose set the JIRA_DEBUG environment variable so custom-commands can auto enable verbose output 2017-08-31 15:09:32 -07:00
Cory Bennett ec0858b09e make jira ISSUE-123 usage call jira view ISSUE-123 2017-08-31 15:09:01 -07:00
Cory Bennett 301a61f2f1 integrate kingpeon library to allow for custom commands via configuration 2017-08-31 15:08:24 -07:00
Cory Bennett b4d7845105 skip editing by default for the transition alias commands (ie done, progress, etc) 2017-08-31 15:04:58 -07:00
Cory Bennett 2a081dd767 use terminal width to adjust list table output 2017-08-30 18:31:49 -07:00
Cory Bennett f52d2c472a set yaml/json tags for option structs 2017-08-27 20:46:25 -07:00
Cory Bennett c89f11d5b5 update generated data files 2017-08-27 19:41:49 -07:00
Cory Bennett 21add544dc automatically login when anonymous user detected 2017-08-27 13:55:48 -07:00
Cory Bennett 1f345cedee refactor trivial objects in favor of arguments to static functions 2017-08-27 00:47:11 -07:00
Cory Bennett b68c44384f merge in edit template changes from v1 branch #105 2017-08-25 22:07:56 -07:00
Cory Bennett 5716a7cb59 set JIRA_OPERATION when parsing configs. Use figtree config types for options to make defaulting work 2017-08-24 16:57:21 -07:00
Cory Bennett 5d6170a81a add --browse option to more commands 2017-08-24 12:39:11 -07:00
Cory Bennett 89e3306254 set defaults in structs rather than kingpin so they can be overriden by configs 2017-08-24 10:59:58 -07:00
Cory Bennett b235dcc384 add better handing for usage error 2017-08-21 22:54:34 -05:00
Cory Bennett 56b1c9df04 adding request command, removing dead code 2017-08-21 09:33:57 -05:00
Cory Bennett a1c28495a7 adding Do required for request language 2017-08-21 09:33:07 -05:00
Cory Bennett a91b9d56b0 add browse command and implement -b option for most operations 2017-08-20 23:19:43 -05:00
Cory Bennett f32cc7079c fix IssueAssign 2017-08-20 23:08:45 -05:00
Cory Bennett 19d8686c01 merge in update for upstream changes #104 2017-08-20 22:51:32 -05:00
coryb 000b82fa19 Merge pull request #104 from wrouesnel/keyring-update
Handle keyring.ErrNotFound error instead of panicing.
2017-08-20 20:49:58 -07:00
Will Rouesnel 07854d6be5 Handle keyring.ErrNotFound error instead of panicing.
github.com/tmc/keyring returns keyring.ErrNotFound instead of a blank password
in newer libraries. Handle this case properly by nulling the error, to give the
user a chance to set a new password.
2017-08-21 13:24:10 +10:00
Cory Bennett abaad5611d add export-templates command 2017-08-20 22:12:55 -05:00
Cory Bennett da39323adf add issuetypes command 2017-08-19 23:55:38 -05:00
Cory Bennett 0bd3ca2fab add components command 2017-08-19 23:39:04 -05:00
Cory Bennett cc90610e72 add component add command 2017-08-19 22:57:41 -05:00
Cory Bennett 959524af23 add take, unassign and assign|give commands 2017-08-19 16:02:29 -07:00
Cory Bennett 916186161a adding labels [add|set|remove] commands 2017-08-19 15:09:47 -07:00
Cory Bennett 47a5ce25bc add issue to cmd options 2017-08-19 15:09:01 -07:00
Cory Bennett f0b08c5869 add comment command 2017-08-19 15:08:34 -07:00
Cory Bennett ec0ac3c4b8 add watch command 2017-08-14 08:59:59 -07:00
Cory Bennett 8b863d297b add rank ISSUE after|before ISSUE command 2017-08-13 23:54:22 -07:00
Cory Bennett a08c92f3aa add vote command 2017-08-13 23:08:40 -07:00
Cory Bennett 37f81a4631 add issuelinktypes command 2017-08-13 22:38:22 -07:00
Cory Bennett aacc9f44e4 add issuelink command 2017-08-13 22:37:56 -07:00
Cory Bennett fc696c3323 fix closing duplicate issue on dup command 2017-08-13 21:10:23 -07:00
Cory Bennett 36632a52f0 rewrite checkpoint 2017-08-13 18:23:38 -07:00
coryb b00021ccbd Merge pull request #90 from bbaugher/master
Use name field for fix version name value. Fixes #89
2017-07-11 14:08:15 -07:00
Bryan Baugher 2f8fecbbfe Use name field for fix version name value. Fixes #89 2017-07-11 16:03:39 -05:00
Cory Bennett 37b138376b Updated Changelog 2017-05-10 09:05:30 -07:00
Cory Bennett 8a5e588ce2 fix unsafe casting for --quiet flag 2017-05-10 09:03:47 -07:00
Cory Bennett 67c86e4858 [#80] add jira unassign and jira give ISSUE --default commands 2017-05-10 08:58:53 -07:00
Cory Bennett 445f8f1f84 Updated Changelog 2017-04-24 11:06:39 -07:00
Cory Bennett 485f73181c revert update 2017-04-24 11:06:31 -07:00
Cory Bennett 4d321ec202 work around github.com/tmc/keyring compile error for windows 2017-04-24 10:50:30 -07:00
Cory Bennett d7bce222b6 Updated Changelog 2017-04-24 10:30:33 -07:00
coryb f231f55d74 Merge pull request #78 from davidreuss/generic-issuelink
Added generic issuelink command
2017-04-24 09:10:31 -07:00
coryb 28242c9c7e Merge pull request #77 from davidreuss/fix-start-parameter-for-pagination
Added --start parameter for pagination on results
2017-04-24 09:07:05 -07:00
David Reuss 05951f1c0d Added generic issuelink command
This allows adding generic links, and could replace 'blocks', and 'dups'
command, since it's pretty much just a copy/paste job.

Usage will be something like:

$ jira issuelink $INWARDISSUE "Relates" OUTWARDISSUE

Pulling the list of the names, for your issuelinktypes

$ jira issuelinktypes | jq '.issueLinkTypes | map(.name)'
[
  "Blocks",
  "Bonfire testing",
  "Clones",
  "Deprecates",
  "Duplicate",
  "Relates",
  "Risks"
]
2017-04-24 12:45:24 +02:00
David Reuss f47563048b Added --start parameter for pagination on results 2017-04-24 11:33:17 +02:00
Cory Bennett cd9976ae4e Updated Changelog 2017-03-22 22:25:25 -07:00
coryb f3aa2f4c1a Merge pull request #74 from clausb/BrowseOnWindows
Implement "browse" subcommand on Windows
2017-03-22 22:24:50 -07:00
Claus Brod f6230ca8c6 Implement "browse" subcommand on Windows 2017-03-22 22:36:21 +01:00
Cory Bennett 412174f8a9 Updated Changelog 2017-02-26 22:58:28 -08:00
Cory Bennett 52085417e6 [#69] add subtask command 2017-02-26 22:38:47 -08:00
Cory Bennett 7a2490c0e6 Updated Changelog 2017-02-08 08:45:42 -08:00
Cory Bennett 437532ae89 minor tweak to 'all' target 2017-02-08 08:45:09 -08:00
coryb 69b565eeaa Merge pull request #65 from mlbright/patch-1
Update README.md
2017-01-24 22:26:17 -08:00
Martin-Louis Bright cc393a3498 Update README.md 2017-01-24 21:35:20 -05:00
coryb c6ba4c681b Merge pull request #64 from astrostl/patch-2
Noting implied usage for the keyring provider
2017-01-05 14:55:15 -08:00
Cory Bennett 63bc2ae15a Merge branch 'master' of github.com:Netflix-Skunkworks/go-jira 2017-01-05 14:54:09 -08:00
Cory Bennett 7d6a5d143d fix random sort ordering in "watchers" response 2017-01-05 14:53:44 -08:00
Justin Honold 0ca0f09aa8 Noting implied usage for the keyring provider 2017-01-05 15:06:25 -06:00
coryb 75242a5204 Merge pull request #62 from astrostl/patch-1
Doc tweak: add info for setting username
2017-01-05 12:55:39 -08:00
Justin Honold e6faa4eab1 Doc tweak: add info for setting username
Couple sentences of cleanup
2017-01-05 14:41:08 -06:00
Cory Bennett 9b53a617a7 update .bashrc for GPG 2016-12-21 17:46:22 -08:00
Cory Bennett d5eed3a635 set GPG_TTY in .bashrc 2016-12-19 16:07:33 -08:00
Cory Bennett 4017339b56 fix typos 2016-12-18 22:41:13 -08:00
Cory Bennett a40b17deed update docs for authentication 2016-12-18 22:04:25 -08:00
Cory Bennett 33807cbbec run full test suite now 2016-12-18 15:56:53 -08:00
Cory Bennett 989c072b94 force password in case password already exists 2016-12-18 15:53:00 -08:00
Cory Bennett d187eee826 fix gpg file permissions after checkout 2016-12-18 15:43:55 -08:00
Cory Bennett 6d34ef3f28 debugging travis 2016-12-18 15:37:23 -08:00
Cory Bennett 7852883202 tweak how prove is run 2016-12-18 15:15:46 -08:00
Cory Bennett cb70941aad refactor password source, allow for "pass" to be used, update tests to use password-source: pass 2016-12-18 15:07:17 -08:00
Cory Bennett 24fd8f6fad update changelog 2016-12-18 09:49:20 -08:00
Cory Bennett ba08d51437 Updated Changelog 2016-12-18 09:48:21 -08:00
Cory Bennett 09d718b5d8 stabbing in the dark 2016-12-17 19:57:45 -08:00
Cory Bennett e3e84d7aa0 only test jira cloud service 2016-12-17 18:52:07 -08:00
Cory Bennett a4f1d754e4 debugging tests 2016-12-17 18:50:20 -08:00
Cory Bennett 683541de1e add verbose test 2016-12-17 18:40:21 -08:00
Cory Bennett e0fd6bab66 only warn about needing login when not already running the login command 2016-12-17 18:32:47 -08:00
Cory Bennett 5ca096ab6e use go-jira.atlassian.net for testing via travis 2016-12-17 17:25:11 -08:00
Cory Bennett ac515e2743 Merge branch 'master' of github.com:Netflix-Skunkworks/go-jira 2016-12-17 14:13:39 -08:00
Cory Bennett be4a5f9156 update for isolated xgo build 2016-12-17 14:13:00 -08:00
coryb 7f10eaa667 Merge pull request #61 from sylus/feature-proxy
fix(http): Add proxy transport
2016-12-17 14:06:45 -08:00
William Hearn b326623ed2 fix(http): Add proxy transport 2016-12-17 16:50:34 -05:00
William Hearn 72c78c6c1c fix(http): Add proxy transport 2016-12-17 14:13:20 -05:00
coryb 5df5a39405 Merge pull request #60 from facundoolano/patch-1
fix typo in readme
2016-12-15 14:00:31 -08:00
Facundo Olano bd54ecc4f6 fix typo in readme 2016-12-15 15:18:11 -03:00
Cory Bennett 073e0bdcce Updated Changelog 2016-11-24 00:11:53 -08:00
Cory Bennett 1347ebe6b6 [#12] integrate with keyring for password storage and provide http basic auth
credentials for every request since most jira services have websudo enabled with
does not allow cookie based authentication
2016-11-23 23:43:29 -08:00
coryb c7565b08a1 Merge pull request #53 from jshirley/master
Add support for a unix socket proxy
2016-09-22 19:14:07 -07:00
Jay Shirley 01067e859c Cleaning up usage 2016-09-22 18:54:52 -07:00
Jay Shirley 8b174625d9 Update usage 2016-09-20 06:46:04 -07:00
Cory Bennett 8acc177627 use gopkg.in for links to maintain version compatibility 2016-09-18 12:44:00 -07:00
Cory Bennett 8d9db0e399 remove extraneous tee 2016-08-29 01:00:35 -07:00
Cory Bennett 998e4601c0 reduce mega verbosity 2016-08-29 01:00:01 -07:00
Cory Bennett 2df70edd00 up plan count for the sleep 2016-08-29 00:58:23 -07:00
Cory Bennett f73b3a5dc8 add missing src directory 2016-08-29 00:49:10 -07:00
Cory Bennett e74c94b030 move start.log to directory 2016-08-29 00:23:05 -07:00
Cory Bennett c18d2140e4 try travis cache and log atlas-run start output 2016-08-29 00:19:27 -07:00
Cory Bennett 2b56833c1c break setup to debug in travis 2016-08-29 00:04:21 -07:00
Cory Bennett fe69ad1cec wait for docker service to start? 2016-08-28 23:47:34 -07:00
Cory Bennett 3b18a1863c make maven cache directory local so we can ensure it is always there for travis testing 2016-08-28 23:36:37 -07:00
Cory Bennett d6d6578b11 make prove verbose 2016-08-28 23:32:03 -07:00
Cory Bennett 2b433dda40 golint 2016-08-28 23:30:25 -07:00
Cory Bennett 08a24e7dc3 require docker, run prove after build 2016-08-28 23:24:12 -07:00
Cory Bennett a746ddc6fb use altas-run to build jira test service in docker container 2016-08-28 23:10:54 -07:00
Cory Bennett e254435734 add "rank" command allow ordering backlog issues in agile projects 2016-08-26 19:44:38 -07:00
Jay Shirley 14298bfa52 Adding a unixproxy mechanism 2016-08-25 10:27:59 -07:00
Cory Bennett a3633aa537 Updated Changelog 2016-08-24 12:27:42 -07:00
coryb 2a8b6521dc Merge pull request #52 from dbrower/master
Prefer transition names which match exactly
2016-08-24 12:26:56 -07:00
Don Brower 4cc172de6b Prefer transition names which match exactly
Some transitions are substrings of other transitions, and the current
loop sometimes never chose the correct one. For example, it would never
choose "QA" if there were also a transtion "Deploy to QA". Ditto for
"Open" and "Reopen".
2016-08-24 14:36:23 -04:00
Cory Bennett 0dd6061992 update tempates to make them more readable with space trimming added to go-1.6 2016-08-21 23:51:45 -07:00
Cory Bennett 9d12f56332 add simple test for the "table" list output 2016-08-21 23:51:30 -07:00
Cory Bennett 824dd2f725 Updated Changelog 2016-08-21 14:39:54 -07:00
Cory Bennett 657bc59c8f make "worklogs" command print output through template
allow "add worklog" command to open edit template
2016-08-21 14:31:30 -07:00
Cory Bennett ec1914dfde remove extra newline at end of worklogs template 2016-08-21 14:31:11 -07:00
Cory Bennett a22911a3f9 adding worklog related templates 2016-08-21 14:27:50 -07:00
Cory Bennett 1f6191425f add vet and link make targets 2016-08-21 14:22:27 -07:00
Cory Bennett f896555299 Updated Changelog 2016-08-21 12:42:47 -07:00
Cory Bennett e0b2c2d240 add GoDoc badge 2016-08-21 10:24:07 -07:00
Cory Bennett a5cb93f112 add travis badge 2016-08-21 10:16:06 -07:00
Cory Bennett cbbf335439 add travis build 2016-08-21 00:46:03 -07:00
Cory Bennett 92b5e38912 update for golint 2016-08-21 00:45:07 -07:00
Cory Bennett 6260e4964f fix for go vet 2016-08-19 10:19:04 -07:00
Cory Bennett 485d65f12b [#51] add missing data directory with generated structs 2016-08-19 08:52:42 -07:00
Cory Bennett 1f33400288 Updated Changelog 2016-08-12 13:32:13 -07:00
Cory Bennett 37332354b7 ignore t/.jira.d/templates which are exported in 000setup.t 2016-08-12 13:31:16 -07:00
Cory Bennett 7530b309e2 add tests for "Task Management" type projects 2016-08-12 13:30:24 -07:00
Cory Bennett e93bf71fea add tests for "Process Management" type projects 2016-08-12 13:29:56 -07:00
Cory Bennett d022f0ad70 add tests for "Project Management" type projects 2016-08-12 13:29:01 -07:00
Cory Bennett 4d5076230c when running "dups" on a Process Management Project type, you have to start/stop the task to resolve it 2016-08-12 13:27:51 -07:00
Cory Bennett 3cbd2f85a4 allow for defaultResolution option for transition command 2016-08-12 12:13:37 -07:00
Cory Bennett 970876851b add tests for Kanban Software Project type issues 2016-08-10 10:52:45 -07:00
Cory Bennett f74c45d7d7 add tests for Scrum Software Project type issues 2016-08-10 10:52:17 -07:00
Cory Bennett 4e7e52288d add test for Basic Software Project type issues 2016-08-10 10:51:55 -07:00
Cory Bennett 63f41e5e88 add comments 2016-08-10 10:50:04 -07:00
Cory Bennett dbf6a5a265 add extra "mojira" user for testing voting and ownership reassignment 2016-08-10 10:49:11 -07:00
Cory Bennett b2056be287 add "backlog" command for Kanban related Issues 2016-08-10 10:48:02 -07:00
Cory Bennett 6beb941d82 fix --noedit flag with "dups" command 2016-08-09 23:51:04 -07:00
Cory Bennett b297d5a4ef add "votes" and "labels" to default view template 2016-08-09 23:48:28 -07:00
Cory Bennett bf7f38de87 gofmt 2016-08-09 23:16:41 -07:00
Cory Bennett f7ed1ed8d8 gofmt 2016-08-09 23:16:20 -07:00
Cory Bennett 280c0f24b3 add "blockerType" config param, for issueLinkType use for "blocks" command 2016-08-09 23:09:31 -07:00
Cory Bennett 6e296052f5 add "debug" make target 2016-08-09 23:06:46 -07:00
Cory Bennett 189b0d252c update gitter room 2016-08-08 00:10:04 -07:00
Cory Bennett 0e453a45d3 fix typo 2016-08-05 13:11:31 -07:00
Cory Bennett 179596ff12 add gitter badge 2016-08-05 13:09:17 -07:00
Cory Bennett 50bac02419 make the bootstrapping simpler, move more setup to 000setup.t 2016-08-04 11:11:03 -07:00
Cory Bennett 4b7e24a199 fix links 2016-08-03 09:58:00 -07:00
Cory Bennett b9bf8455bd mention osht for testing 2016-08-03 09:56:54 -07:00
Cory Bennett 9111231545 start adding some basic integration tests 2016-08-03 00:13:53 -07:00
Cory Bennett 986528d4ea default issuetype to "Bug" for project that have Bug, otherwise try "Task" 2016-08-03 00:09:09 -07:00
Cory Bennett 9c1f028be2 make view template only show fields that have values 2016-08-02 23:16:08 -07:00
Cory Bennett e3c5051e5e make default create template only display fields if they are valid fields for the project 2016-08-02 22:44:24 -07:00
Cory Bennett 580ea50b37 ignore empty json fields when processing templates 2016-08-02 22:36:51 -07:00
Cory Bennett be31acde65 allow JIRA_LOG_FORMAT env variable to control log output format 2016-08-02 20:12:30 -07:00
Cory Bennett a2f8b7ef65 remove extraneous debug 2016-08-02 20:06:40 -07:00
Cory Bennett c28d46fe8f add logout command
modify password prompt to echo masked password
2016-08-02 20:04:11 -07:00
Cory Bennett 108a5b4976 tweak cookies to store hostname
dump all http request/response with --verbose
2016-08-02 19:23:24 -07:00
Cory Bennett e3d11357e1 load configs in order of closest to cwd (/etc/go-jira.yml is last) 2016-08-02 19:20:23 -07:00
Cory Bennett dfb10740f5 [#48] fix quoting for "make install" so it actually installs to homedir 2016-08-01 11:31:06 -07:00
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
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 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
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
831 changed files with 242448 additions and 2241 deletions
+10 -7
View File
@@ -1,7 +1,10 @@
bin/
pkg/
src/code.google.com/
src/github.com/docopt/
src/github.com/mgutz/
src/github.com/op/
src/gopkg.in/
jira
schemas/*.json
t/.gnupg/random_seed
t/issue.props
t/attach.props
t/garbage.bin
t/attach1.txt
t/binary.out
t/foobar.bin
dist
+11
View File
@@ -0,0 +1,11 @@
config:
stop: true
password-source: pass
endpoint: https://go-jira.atlassian.net
user: admin
queries:
todo: |
resolution = unresolved {{if .project}}AND project = '{{.project}}'{{end}} AND status = 'To Do'
open: |
resolution = unresolved {{if .project}}AND project = '{{.project}}'{{end}} AND status = 'Open'
+16
View File
@@ -0,0 +1,16 @@
sudo: true
before_install:
- sudo apt-get update && sudo apt-get install -y pass gnupg
language: go
go_import_path: gopkg.in/Netflix-Skunkworks/go-jira.v1
go:
- 1.8
matrix:
fast_finish: true
script:
- make vet
- make
- make prove 2>&1
+220
View File
@@ -1,5 +1,225 @@
# Changelog
## 1.0.9 - 2017-09-17
* need issuetype to use the default list table template now [Cory Bennett] [[3e8b9bd](https://github.com/Netflix-Skunkworks/go-jira/commit/3e8b9bd)]
* [[#102](https://github.com/Netflix-Skunkworks/go-jira/issues/102)] add issuetype into the default queryfields and add it to the default `table` list template [Cory Bennett] [[c9d8dfb](https://github.com/Netflix-Skunkworks/go-jira/commit/c9d8dfb)]
## 1.0.8 - 2017-09-17
* [[#100](https://github.com/Netflix-Skunkworks/go-jira/issues/100)] add support for posting, fetching, listing and removing attachments [Cory Bennett] [[66eb7bf](https://github.com/Netflix-Skunkworks/go-jira/commit/66eb7bf)]
## 1.0.7 - 2017-09-15
* [[#87](https://github.com/Netflix-Skunkworks/go-jira/issues/87)] add various commands for interacting with epics [Cory Bennett] [[893454f](https://github.com/Netflix-Skunkworks/go-jira/commit/893454f)]
## 1.0.6 - 2017-09-13
* tweaks for templates in named queries to work better [Cory Bennett] [[00cba79](https://github.com/Netflix-Skunkworks/go-jira/commit/00cba79)]
* [[#99](https://github.com/Netflix-Skunkworks/go-jira/issues/99)] add support for named queries to be stored in configs [Cory Bennett] [[fb43753](https://github.com/Netflix-Skunkworks/go-jira/commit/fb43753)]
* [[#98](https://github.com/Netflix-Skunkworks/go-jira/issues/98)] add `--status` option for JQL filter on status with `list` command [Cory Bennett] [[5da04c1](https://github.com/Netflix-Skunkworks/go-jira/commit/5da04c1)]
## 1.0.5 - 2017-09-11
* use --gjq for GJson Query to filter json response data [Cory Bennett] [[608e586](https://github.com/Netflix-Skunkworks/go-jira/commit/608e586)]
* fix field tag syntax [Cory Bennett] [[2c552ac](https://github.com/Netflix-Skunkworks/go-jira/commit/2c552ac)]
* add '{{jira}}' template macro to refer to path of currently running jira command [Cory Bennett] [[941824d](https://github.com/Netflix-Skunkworks/go-jira/commit/941824d)]
## 1.0.4 - 2017-09-08
* update deps for kingpeon update use os.exec instead of syscall.exec for windows [Cory Bennett] [[86b963b](https://github.com/Netflix-Skunkworks/go-jira/commit/86b963b)]
## 1.0.3 - 2017-09-06
* [[#66](https://github.com/Netflix-Skunkworks/go-jira/issues/66)] add --started option to `jira worklog add` to change the start time for worklog [Cory Bennett] [[e6faee1](https://github.com/Netflix-Skunkworks/go-jira/commit/e6faee1)]
* [[#45](https://github.com/Netflix-Skunkworks/go-jira/issues/45)] automatically add comment to issue even if transition does not support comment updates during transtion [Cory Bennett] [[c4be59c](https://github.com/Netflix-Skunkworks/go-jira/commit/c4be59c)]
## 1.0.2 - 2017-09-06
* update dependencies [Cory Bennett] [[aa876cd](https://github.com/Netflix-Skunkworks/go-jira/commit/aa876cd)]
* update for github.com/AlecAivazis/survey => gopkg.in/AlecAivazis/survey.v1 package [Cory Bennett] [[9453179](https://github.com/Netflix-Skunkworks/go-jira/commit/9453179)]
* use stdout to determin output terminal size [Cory Bennett] [[4d79af4](https://github.com/Netflix-Skunkworks/go-jira/commit/4d79af4)]
## 1.0.1 - 2017-09-06
* [[#13](https://github.com/Netflix-Skunkworks/go-jira/issues/13)] change default input syntax to not require escaping for special characters [Cory Bennett] [[1106558](https://github.com/Netflix-Skunkworks/go-jira/commit/1106558)]
## 1.0.0 - 2017-09-05
* fix build for windows [Cory Bennett] [[1b854da](https://github.com/Netflix-Skunkworks/go-jira/commit/1b854da)]
* change the default log output format [Cory Bennett] [[f1b8c64](https://github.com/Netflix-Skunkworks/go-jira/commit/f1b8c64)]
* tweak auto-login so it does not print the standard `jira login` command output [Cory Bennett] [[49f6cdc](https://github.com/Netflix-Skunkworks/go-jira/commit/49f6cdc)]
* add --quiet global option [Cory Bennett] [[c226077](https://github.com/Netflix-Skunkworks/go-jira/commit/c226077)]
* refactor to allow for --insecure and --unixproxy arguments [Cory Bennett] [[c0358eb](https://github.com/Netflix-Skunkworks/go-jira/commit/c0358eb)]
* handle html response on expired cookies (require X-Ausername header to always be present) [Cory Bennett] [[21920c5](https://github.com/Netflix-Skunkworks/go-jira/commit/21920c5)]
* allow login prompt to be interrupted [Cory Bennett] [[7ab6c22](https://github.com/Netflix-Skunkworks/go-jira/commit/7ab6c22)]
* fmt -> log typo [Cory Bennett] [[bccf09f](https://github.com/Netflix-Skunkworks/go-jira/commit/bccf09f)]
* make ~/.jira.d directory if not already present [Cory Bennett] [[e72479c](https://github.com/Netflix-Skunkworks/go-jira/commit/e72479c)]
* fix go vet [Cory Bennett] [[e04b506](https://github.com/Netflix-Skunkworks/go-jira/commit/e04b506)]
* fix tests [Cory Bennett] [[ba35f55](https://github.com/Netflix-Skunkworks/go-jira/commit/ba35f55)]
* add OK printf [Cory Bennett] [[dc02181](https://github.com/Netflix-Skunkworks/go-jira/commit/dc02181)]
* change --method to use -M for backwards compat [Cory Bennett] [[b120c0b](https://github.com/Netflix-Skunkworks/go-jira/commit/b120c0b)]
* add resolution to dup'd issues when necessary [Cory Bennett] [[2638396](https://github.com/Netflix-Skunkworks/go-jira/commit/2638396)]
* call correct function for `labels remove|set` commands [Cory Bennett] [[ad1a62a](https://github.com/Netflix-Skunkworks/go-jira/commit/ad1a62a)]
* data argument is optional (for GET and DELETE requests) [Cory Bennett] [[4b60313](https://github.com/Netflix-Skunkworks/go-jira/commit/4b60313)]
* fix usage, overrides not serialized correctly [Cory Bennett] [[84119a2](https://github.com/Netflix-Skunkworks/go-jira/commit/84119a2)]
* fix `jira ISSUE-123` command line parsing [Cory Bennett] [[fa4ac25](https://github.com/Netflix-Skunkworks/go-jira/commit/fa4ac25)]
* add logger object to jiracmd [Cory Bennett] [[aed952b](https://github.com/Netflix-Skunkworks/go-jira/commit/aed952b)]
* refactor for GlobalOptions and CommonOptions [Cory Bennett] [[979da1f](https://github.com/Netflix-Skunkworks/go-jira/commit/979da1f)]
* move commands from jiracli package to jiracmd package [Cory Bennett] [[0a5510b](https://github.com/Netflix-Skunkworks/go-jira/commit/0a5510b)]
* use jiracli.Error object to disambiguate between kingpin errors and cli errors [Cory Bennett] [[fb1bfeb](https://github.com/Netflix-Skunkworks/go-jira/commit/fb1bfeb)]
* fix stray newline for list table template [Cory Bennett] [[36c26c5](https://github.com/Netflix-Skunkworks/go-jira/commit/36c26c5)]
* fix dynamic table output when not on tty [Cory Bennett] [[3942f6f](https://github.com/Netflix-Skunkworks/go-jira/commit/3942f6f)]
* when using --verbose set the JIRA_DEBUG environment variable so custom-commands can auto enable verbose output [Cory Bennett] [[da9a2b2](https://github.com/Netflix-Skunkworks/go-jira/commit/da9a2b2)]
* make `jira ISSUE-123` usage call `jira view ISSUE-123` [Cory Bennett] [[ec0858b](https://github.com/Netflix-Skunkworks/go-jira/commit/ec0858b)]
* integrate kingpeon library to allow for custom commands via configuration [Cory Bennett] [[301a61f](https://github.com/Netflix-Skunkworks/go-jira/commit/301a61f)]
* use terminal width to adjust list table output [Cory Bennett] [[2a081dd](https://github.com/Netflix-Skunkworks/go-jira/commit/2a081dd)]
* set yaml/json tags for option structs [Cory Bennett] [[f52d2c4](https://github.com/Netflix-Skunkworks/go-jira/commit/f52d2c4)]
* update generated data files [Cory Bennett] [[c89f11d](https://github.com/Netflix-Skunkworks/go-jira/commit/c89f11d)]
* automatically login when anonymous user detected [Cory Bennett] [[21add54](https://github.com/Netflix-Skunkworks/go-jira/commit/21add54)]
* refactor trivial objects in favor of arguments to static functions [Cory Bennett] [[1f345ce](https://github.com/Netflix-Skunkworks/go-jira/commit/1f345ce)]
* set JIRA_OPERATION when parsing configs. Use figtree config types for options to make defaulting work [Cory Bennett] [[5716a7c](https://github.com/Netflix-Skunkworks/go-jira/commit/5716a7c)]
* add better handing for usage error [Cory Bennett] [[b235dcc](https://github.com/Netflix-Skunkworks/go-jira/commit/b235dcc)]
* adding `request` command, removing dead code [Cory Bennett] [[56b1c9d](https://github.com/Netflix-Skunkworks/go-jira/commit/56b1c9d)]
* adding Do required for request language [Cory Bennett] [[a1c2849](https://github.com/Netflix-Skunkworks/go-jira/commit/a1c2849)]
* add `browse` command and implement -b option for most operations [Cory Bennett] [[a91b9d5](https://github.com/Netflix-Skunkworks/go-jira/commit/a91b9d5)]
* fix IssueAssign [Cory Bennett] [[f32cc70](https://github.com/Netflix-Skunkworks/go-jira/commit/f32cc70)]
* merge in update for upstream changes [#104](https://github.com/Netflix-Skunkworks/go-jira/issues/104) [Cory Bennett] [[19d8686](https://github.com/Netflix-Skunkworks/go-jira/commit/19d8686)]
* add `export-templates` command [Cory Bennett] [[abaad56](https://github.com/Netflix-Skunkworks/go-jira/commit/abaad56)]
* add `issuetypes` command [Cory Bennett] [[da39323](https://github.com/Netflix-Skunkworks/go-jira/commit/da39323)]
* add `components` command [Cory Bennett] [[0bd3ca2](https://github.com/Netflix-Skunkworks/go-jira/commit/0bd3ca2)]
* add `component add` command [Cory Bennett] [[cc90610](https://github.com/Netflix-Skunkworks/go-jira/commit/cc90610)]
* add `take`, `unassign` and `assign|give` commands [Cory Bennett] [[959524a](https://github.com/Netflix-Skunkworks/go-jira/commit/959524a)]
* adding `labels [add|set|remove]` commands [Cory Bennett] [[9161861](https://github.com/Netflix-Skunkworks/go-jira/commit/9161861)]
* add `comment` command [Cory Bennett] [[f0b08c5](https://github.com/Netflix-Skunkworks/go-jira/commit/f0b08c5)]
* add `watch` command [Cory Bennett] [[ec0ac3c](https://github.com/Netflix-Skunkworks/go-jira/commit/ec0ac3c)]
* add `rank ISSUE after|before ISSUE` command [Cory Bennett] [[8b863d2](https://github.com/Netflix-Skunkworks/go-jira/commit/8b863d2)]
* add `vote` command [Cory Bennett] [[a08c92f](https://github.com/Netflix-Skunkworks/go-jira/commit/a08c92f)]
* add `issuelinktypes` command [Cory Bennett] [[37f81a4](https://github.com/Netflix-Skunkworks/go-jira/commit/37f81a4)]
* add `issuelink` command [Cory Bennett] [[aacc9f4](https://github.com/Netflix-Skunkworks/go-jira/commit/aacc9f4)]
* fix closing duplicate issue on `dup` command [Cory Bennett] [[fc696c3](https://github.com/Netflix-Skunkworks/go-jira/commit/fc696c3)]
* rewrite checkpoint [Cory Bennett] [[36632a5](https://github.com/Netflix-Skunkworks/go-jira/commit/36632a5)]
## 0.1.14 - 2017-05-10
* fix unsafe casting for --quiet flag [Cory Bennett] [[6f29f43](https://github.com/Netflix-Skunkworks/go-jira/commit/6f29f43)]
* [[#80](https://github.com/Netflix-Skunkworks/go-jira/issues/80)] add `jira unassign` and `jira give ISSUE --default` commands [Cory Bennett] [[03d8633](https://github.com/Netflix-Skunkworks/go-jira/commit/03d8633)]
## 0.1.13 - 2017-04-24
* work around `github.com/tmc/keyring` compile error for windows [Cory Bennett] [[85298e9](https://github.com/Netflix-Skunkworks/go-jira/commit/85298e9)]
* Added generic issuelink command [David Reuss] [[cc54d11](https://github.com/Netflix-Skunkworks/go-jira/commit/cc54d11)]
* Added --start parameter for pagination on results [David Reuss] [[9b94d9e](https://github.com/Netflix-Skunkworks/go-jira/commit/9b94d9e)]
## 0.1.12 - 2017-03-22
* Implement "browse" subcommand on Windows [Claus Brod] [[ca333d8](https://github.com/Netflix-Skunkworks/go-jira/commit/ca333d8)]
## 0.1.11 - 2017-02-26
* [[#69](https://github.com/Netflix-Skunkworks/go-jira/issues/69)] add subtask command [Cory Bennett] [[21a2ed5](https://github.com/Netflix-Skunkworks/go-jira/commit/21a2ed5)]
## 0.1.10 - 2017-02-08
* set GPG_TTY in .bashrc [Cory Bennett] [[b1e552f](https://github.com/Netflix-Skunkworks/go-jira/commit/b1e552f)]
* force password in case password already exists [Cory Bennett] [[d5a2c3b](https://github.com/Netflix-Skunkworks/go-jira/commit/d5a2c3b)]
* refactor password source, allow for "pass" to be used, update tests to use `password-source: pass` [Cory Bennett] [[5a71939](https://github.com/Netflix-Skunkworks/go-jira/commit/5a71939)]
## 0.1.9 - 2016-12-18
* only warn about needing login when not already running the login command [Cory Bennett] [[6c24e55](https://github.com/Netflix-Skunkworks/go-jira/commit/6c24e55)]
* fix(http): Add proxy transport [William Hearn] [[4bd740b](https://github.com/Netflix-Skunkworks/go-jira/commit/4bd740b)] [[2dff6c9](https://github.com/Netflix-Skunkworks/go-jira/commit/2dff6c9)]
## 0.1.8 - 2016-11-24
* [[#12](https://github.com/Netflix-Skunkworks/go-jira/issues/12)] integrate with keyring for password storage and provide http basic auth credentials for every request since most jira services have websudo enabled with does not allow cookie based authentication [Cory Bennett] [[b8a6e57](https://github.com/Netflix-Skunkworks/go-jira/commit/b8a6e57)]
* Cleaning up usage [Jay Shirley] [[8add52b](https://github.com/Netflix-Skunkworks/go-jira/commit/8add52b)]
* Update usage [Jay Shirley] [[b56e32a](https://github.com/Netflix-Skunkworks/go-jira/commit/b56e32a)]
* use gopkg.in for links to maintain version compatibility [Cory Bennett] [[1414d1f](https://github.com/Netflix-Skunkworks/go-jira/commit/1414d1f)]
* golint [Cory Bennett] [[44cdebf](https://github.com/Netflix-Skunkworks/go-jira/commit/44cdebf)]
* add "rank" command allow ordering backlog issues in agile projects [Cory Bennett] [[e4cc9c6](https://github.com/Netflix-Skunkworks/go-jira/commit/e4cc9c6)]
* Adding a unixproxy mechanism [Jay Shirley] [[5b9c0dd](https://github.com/Netflix-Skunkworks/go-jira/commit/5b9c0dd)]
## 0.1.7 - 2016-08-24
* Prefer transition names which match exactly [Don Brower] [[e40f9c1](https://github.com/Netflix-Skunkworks/go-jira/commit/e40f9c1)]
* update tempates to make them more readable with space trimming added to go-1.6 [Cory Bennett] [[693b3e4](https://github.com/Netflix-Skunkworks/go-jira/commit/693b3e4)]
## 0.1.6 - 2016-08-21
* make "worklogs" command print output through template allow "add worklog" command to open edit template [Cory Bennett] [[cc3fbee](https://github.com/Netflix-Skunkworks/go-jira/commit/cc3fbee)]
* remove extra newline at end of worklogs template [Cory Bennett] [[d08ef15](https://github.com/Netflix-Skunkworks/go-jira/commit/d08ef15)]
* adding worklog related templates [Cory Bennett] [[ab1cd27](https://github.com/Netflix-Skunkworks/go-jira/commit/ab1cd27)]
## 0.1.5 - 2016-08-21
* update for golint [Cory Bennett] [[5a4e17c](https://github.com/Netflix-Skunkworks/go-jira/commit/5a4e17c)]
* fix for go vet [Cory Bennett] [[355fb42](https://github.com/Netflix-Skunkworks/go-jira/commit/355fb42)]
## 0.1.4 - 2016-08-12
* when running "dups" on a Process Management Project type, you have to start/stop the task to resolve it [Cory Bennett] [[2c91905](https://github.com/Netflix-Skunkworks/go-jira/commit/2c91905)]
* allow for defaultResolution option for transition command [Cory Bennett] [[a328c2d](https://github.com/Netflix-Skunkworks/go-jira/commit/a328c2d)]
* add "backlog" command for Kanban related Issues [Cory Bennett] [[5d39b23](https://github.com/Netflix-Skunkworks/go-jira/commit/5d39b23)]
* fix --noedit flag with "dups" command [Cory Bennett] [[37c07fa](https://github.com/Netflix-Skunkworks/go-jira/commit/37c07fa)]
* add "votes" and "labels" to default view template [Cory Bennett] [[6f73b8c](https://github.com/Netflix-Skunkworks/go-jira/commit/6f73b8c)]
* add "blockerType" config param, for issueLinkType use for "blocks" command [Cory Bennett] [[30fd301](https://github.com/Netflix-Skunkworks/go-jira/commit/30fd301)]
* update gitter room [Cory Bennett] [[4b822b1](https://github.com/Netflix-Skunkworks/go-jira/commit/4b822b1)]
* default issuetype to "Bug" for project that have Bug, otherwise try "Task" [Cory Bennett] [[0c807b4](https://github.com/Netflix-Skunkworks/go-jira/commit/0c807b4)]
* make view template only show fields that have values [Cory Bennett] [[8238fe8](https://github.com/Netflix-Skunkworks/go-jira/commit/8238fe8)]
* make default create template only display fields if they are valid fields for the project [Cory Bennett] [[adc2ace](https://github.com/Netflix-Skunkworks/go-jira/commit/adc2ace)]
* ignore empty json fields when processing templates [Cory Bennett] [[f5f3e28](https://github.com/Netflix-Skunkworks/go-jira/commit/f5f3e28)]
* allow JIRA_LOG_FORMAT env variable to control log output format [Cory Bennett] [[469def0](https://github.com/Netflix-Skunkworks/go-jira/commit/469def0)]
* remove extraneous debug [Cory Bennett] [[752a94d](https://github.com/Netflix-Skunkworks/go-jira/commit/752a94d)]
* add logout command modify password prompt to echo masked password [Cory Bennett] [[8ad91be](https://github.com/Netflix-Skunkworks/go-jira/commit/8ad91be)]
* tweak cookies to store hostname dump all http request/response with --verbose [Cory Bennett] [[f93fe79](https://github.com/Netflix-Skunkworks/go-jira/commit/f93fe79)]
* load configs in order of closest to cwd (/etc/go-jira.yml is last) [Cory Bennett] [[f54267b](https://github.com/Netflix-Skunkworks/go-jira/commit/f54267b)]
## 0.1.3 - 2016-07-30
* [[#43](https://github.com/Netflix-Skunkworks/go-jira/issues/43)] add support for jira done|todo|prog commands [Cory Bennett] [[dd7d1cc](https://github.com/Netflix-Skunkworks/go-jira/commit/dd7d1cc)]
* Reporter is not generally editable. [Mike Pountney] [[a637b43](https://github.com/Netflix-Skunkworks/go-jira/commit/a637b43)]
## 0.1.2 - 2016-06-29
* [[#44](https://github.com/Netflix-Skunkworks/go-jira/issues/44)] Close tmpfile before rename to work around "The process cannot access the file because it is being used by another process" error on windows. [Cory Bennett] [[0980f8e](https://github.com/Netflix-Skunkworks/go-jira/commit/0980f8e)]
## 0.1.1 - 2016-06-28
* use USERPROFILE instead of HOME for windows, rework paths to use filepath.Join for better cross platform support [Cory Bennett] [[adcedc4](https://github.com/Netflix-Skunkworks/go-jira/commit/adcedc4)]
* Include templates from a system path [Mike Pountney] [[cf10f53](https://github.com/Netflix-Skunkworks/go-jira/commit/cf10f53)]
* Added support for the ```expand``` option for Issues [tobyjoe] [[fb4afc9](https://github.com/Netflix-Skunkworks/go-jira/commit/fb4afc9)]
* change for api changes to go-logging [Cory Bennett] [[7bfc6e8](https://github.com/Netflix-Skunkworks/go-jira/commit/7bfc6e8)]
* Fix issuetype calls adding URL escaping [Jonathan Wright] [[e4a25e2](https://github.com/Netflix-Skunkworks/go-jira/commit/e4a25e2)]
## 0.1.0 - 2016-01-29
* Fixes [#32](https://github.com/Netflix-Skunkworks/go-jira/issues/32) - make path to cookieFile if it's not present [Mike Pountney] [[6644579](https://github.com/Netflix-Skunkworks/go-jira/commit/6644579)]
* Add component/components support: add and list for now. [Mike Pountney] [[d7b3226](https://github.com/Netflix-Skunkworks/go-jira/commit/d7b3226)]
* Tweak the CmdWatch contract and add watcher remove support [Mike Pountney] [[383847a](https://github.com/Netflix-Skunkworks/go-jira/commit/383847a)]
* Amend vote/unvote to be vote/vote --down [Mike Pountney] [[797edef](https://github.com/Netflix-Skunkworks/go-jira/commit/797edef)]
* Add 'vote' and 'unvote' [Mike Pountney] [[c95e66e](https://github.com/Netflix-Skunkworks/go-jira/commit/c95e66e)]
## 0.0.20 - 2016-01-21
* [issue [#28](https://github.com/Netflix-Skunkworks/go-jira/issues/28)] check to make sure we got back issuetypes for create metadata [Cory Bennett] [[ee0e780](https://github.com/Netflix-Skunkworks/go-jira/commit/ee0e780)]
* Add insecure option for TLS endpoints [Brian Lalor] [[6a88bb9](https://github.com/Netflix-Skunkworks/go-jira/commit/6a88bb9)]
* Correct naming of parameter: set/add/remove are actions. [Mike Pountney] [[303784f](https://github.com/Netflix-Skunkworks/go-jira/commit/303784f)]
* Tweak CmdLabels args so that magic happens with CLI [Mike Pountney] [[40a7c65](https://github.com/Netflix-Skunkworks/go-jira/commit/40a7c65)]
* Expose ViewTicket as per FindIssues [Mike Pountney] [[8977f3d](https://github.com/Netflix-Skunkworks/go-jira/commit/8977f3d)]
* Add exposed versions of getTemplate and runTemplate [Mike Pountney] [[da6cbd5](https://github.com/Netflix-Skunkworks/go-jira/commit/da6cbd5)]
* Add 'labels' command to set/add/remove labels [Mike Pountney] [[230b52d](https://github.com/Netflix-Skunkworks/go-jira/commit/230b52d)]
* Add a 'join' func to the template engine [Mike Pountney] [[a7820fe](https://github.com/Netflix-Skunkworks/go-jira/commit/a7820fe)]
* make "jira" golang package, move code from jira/cli to root, move jira/main.go to main/main.go [Cory Bennett] [[7268b9e](https://github.com/Netflix-Skunkworks/go-jira/commit/7268b9e)]
## 0.0.19 - 2015-12-09
* fix jira trans TRANS ISSUE (case sensitivity issue), also go fmt [Cory Bennett] [[3c30f3b](https://github.com/Netflix-Skunkworks/go-jira/commit/3c30f3b)]
## 0.0.18 - 2015-12-03
* need to default "quiet" to false [Cory Bennett] [[4f4a89b](https://github.com/Netflix-Skunkworks/go-jira/commit/4f4a89b)]
## 0.0.17 - 2015-12-03
* add --quiet command to not print the OK .. add --saveFile option to print the issue/link to a file on create command [Cory Bennett] [[c9ac162](https://github.com/Netflix-Skunkworks/go-jira/commit/c9ac162)]
Generated
+165
View File
@@ -0,0 +1,165 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/alecthomas/template"
packages = [".","parse"]
revision = "a0175ee3bccc567396460bf5acd36800cb10c49c"
[[projects]]
branch = "master"
name = "github.com/alecthomas/units"
packages = ["."]
revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
[[projects]]
branch = "master"
name = "github.com/cheekybits/genny"
packages = ["generic"]
revision = "9127e812e1e9e501ce899a18121d316ecb52e4ba"
[[projects]]
branch = "master"
name = "github.com/coryb/figtree"
packages = ["."]
revision = "c7d8fbf1d7746b5864b8262fabffec813b5a43fa"
[[projects]]
branch = "master"
name = "github.com/coryb/kingpeon"
packages = ["."]
revision = "64b561ae2d0f895b94719c486bed798f4236a4b3"
[[projects]]
branch = "master"
name = "github.com/coryb/oreo"
packages = ["."]
revision = "95687d61c95ee1522c1140e2af59b0c1846abfc1"
[[projects]]
branch = "master"
name = "github.com/fatih/camelcase"
packages = ["."]
revision = "f6a740d52f961c60348ebb109adde9f4635d7540"
[[projects]]
branch = "master"
name = "github.com/guelfey/go.dbus"
packages = ["."]
revision = "f6a3a2366cc39b8479cadc499d3c735fb10fbdda"
[[projects]]
branch = "master"
name = "github.com/jinzhu/copier"
packages = ["."]
revision = "32e0d0db1dcd4373fb9eb0f9d727b1fe1a723e54"
[[projects]]
branch = "master"
name = "github.com/kballard/go-shellquote"
packages = ["."]
revision = "cd60e84ee657ff3dc51de0b4f55dd299a3e136f2"
[[projects]]
branch = "master"
name = "github.com/mattn/go-colorable"
packages = ["."]
revision = "ad5389df28cdac544c99bd7b9161a0b5b6ca9d1b"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe"
version = "v0.0.2"
[[projects]]
branch = "master"
name = "github.com/mgutz/ansi"
packages = ["."]
revision = "9520e82c474b0a04dd04f8a40959027271bab992"
[[projects]]
branch = "master"
name = "github.com/pkg/browser"
packages = ["."]
revision = "c90ca0c84f15f81c982e32665bffd8d7aac8f097"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
branch = "master"
name = "github.com/sethgrid/pester"
packages = ["."]
revision = "a86a2d88f4dc3c7dbf3a6a6bbbfb095690b834b6"
[[projects]]
branch = "master"
name = "github.com/theckman/go-flock"
packages = ["."]
revision = "6de226b0d5f040ed85b88c82c381709b98277f3d"
[[projects]]
branch = "master"
name = "github.com/tidwall/gjson"
packages = ["."]
revision = "be96719f990978a867f52c48f29d43f6b591da28"
[[projects]]
branch = "master"
name = "github.com/tidwall/match"
packages = ["."]
revision = "173748da739a410c5b0b813b956f89ff94730b4c"
[[projects]]
branch = "master"
name = "github.com/tmc/keyring"
packages = ["."]
revision = "06e6283d50adc5f8fcdb3cdf33ee1244d4400ae1"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
revision = "9ba3862cf6a5452ae579de98f9364dd2e544844c"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix","windows"]
revision = "a5054c7c1385fd50d9394475365355a87a7873ec"
[[projects]]
name = "gopkg.in/AlecAivazis/survey.v1"
packages = [".","core","terminal"]
revision = "9d910423e24aa6d7c7950160658c295e0734c7e0"
version = "1.3.1"
[[projects]]
name = "gopkg.in/alecthomas/kingpin.v2"
packages = ["."]
revision = "1087e65c9441605df944fb12c33f0fe7072d18ca"
version = "v2.2.5"
[[projects]]
branch = "v2"
name = "gopkg.in/coryb/yaml.v2"
packages = ["."]
revision = "fb7cb9628c6e3bdd76c29fb91798d51a09832470"
[[projects]]
name = "gopkg.in/op/go-logging.v1"
packages = ["."]
revision = "b2cb9fa56473e98db8caba80237377e83fe44db5"
version = "v1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "1f4b97fcf898a5ef03af5e222686a09328b393e71797d21bf0c37b74d1e74a8e"
solver-name = "gps-cdcl"
solver-version = 1
+75
View File
@@ -0,0 +1,75 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
name = "github.com/coryb/figtree"
[[constraint]]
name = "github.com/coryb/kingpeon"
[[constraint]]
name = "github.com/coryb/oreo"
[[constraint]]
name = "github.com/jinzhu/copier"
[[constraint]]
name = "github.com/kballard/go-shellquote"
[[constraint]]
name = "github.com/mgutz/ansi"
[[constraint]]
name = "github.com/pkg/browser"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
name = "github.com/savaki/jq"
[[constraint]]
name = "github.com/tmc/keyring"
[[constraint]]
name = "golang.org/x/crypto"
[[constraint]]
name = "gopkg.in/AlecAivazis/survey.v1"
version = "1.3.1"
[[constraint]]
name = "gopkg.in/alecthomas/kingpin.v2"
version = "2.2.5"
[[constraint]]
name = "gopkg.in/coryb/yaml.v2"
[[constraint]]
name = "gopkg.in/op/go-logging.v1"
version = "1.0.0"
[[constraint]]
branch = "master"
name = "github.com/tidwall/gjson"
+67 -48
View File
@@ -1,72 +1,91 @@
PLATFORMS= \
freebsd-386 \
freebsd-amd64 \
freebsd-arm \
linux-386 \
linux-amd64 \
linux-arm \
openbsd-386 \
openbsd-amd64 \
windows-386 \
windows-amd64 \
darwin-386 \
darwin-amd64 \
$(NULL)
DIST=$(shell pwd)/dist
export GOPATH=$(shell pwd)
GOBIN ?= $(shell pwd)/bin
NAME=jira
CURVER ?= $(shell [ -d .git ] && git describe --abbrev=0 --tags || grep ^\#\# CHANGELOG.md | awk '{print $$2; exit}')
LDFLAGS:=-X main.buildVersion=$(CURVER)
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
build: src/github.com/Netflix-Skunkworks/go-jira
go build -ldflags "$(LDFLAGS)" -o $(GOBIN)/$(NAME) jira/main.go
DIST=$(CWD)$(SEP)dist
src/%:
mkdir -p $(@D)
test -L $@ || ln -sf ../../.. $@
go get -v $*/jira
GOBIN ?= $(CWD)
cross-setup:
for p in $(PLATFORMS); do \
echo "Building for $$p"; \
cd $(GOROOT)/src && sudo GOROOT_BOOTSTRAP=$(GOROOT) GOOS=$${p/-*/} GOARCH=$${p/*-/} bash ./make.bash --no-clean; \
done
CURVER ?= $(patsubst v%,%,$(shell [ -d .git ] && git describe --abbrev=0 --tags || grep ^\#\# CHANGELOG.md | awk '{print $$2; exit}'))
LDFLAGS:= -w
build:
go build -gcflags="-e" -v -ldflags "$(LDFLAGS) -s" -o '$(BIN)' cmd/jira/main.go
vet:
@go vet .
@go vet ./jiracli
@go vet ./jiracmd
@go vet ./jiradata
@go vet ./cmd/jira
lint:
@go get github.com/golang/lint/golint
@golint .
@golint ./jiracli
@golint ./jiracmd
@golint ./jiradata
@golint ./cmd/jira
all:
rm -rf $(DIST); \
mkdir -p $(DIST); \
for p in $(PLATFORMS); do \
echo "Building for $$p"; \
GOOS=$${p/-*/} GOARCH=$${p/*-/} go build -v -ldflags "$(LDFLAGS) -s" -o $(DIST)/$(NAME)-$$p jira/main.go ; \
done
fmt:
gofmt -s -w jira
go get -u github.com/karalabe/xgo
rm -rf dist
mkdir -p dist
xgo --targets="freebsd/amd64,linux/386,linux/amd64,windows/386,windows/amd64,darwin/amd64" -dest ./dist -ldflags="-w -s" ./cmd/jira
install:
export GOBIN=~/bin && ${MAKE} build
${MAKE} GOBIN=$$HOME/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 ^v$(CURVER) HEAD *.go jiracli/*.go jiradata/*.go jiracmd/*.go cmd/*/*.go *.lock | grep -vE 'gofmt|go fmt|version bump'
update-changelog:
update-changelog:
@echo "# Changelog" > CHANGELOG.md.new; \
echo >> CHANGELOG.md.new; \
echo "## $(NEWVER) - $(TODAY)" >> CHANGELOG.md.new; \
echo >> CHANGELOG.md.new; \
$(MAKE) changes | \
$(MAKE) --no-print-directory --silent changes | \
perl -pe 's{\[([a-f0-9]+)\]}{[[$$1](https://github.com/Netflix-Skunkworks/go-jira/commit/$$1)]}g' | \
perl -pe 's{\#(\d+)}{[#$$1](https://github.com/Netflix-Skunkworks/go-jira/issues/$$1)}g' >> CHANGELOG.md.new; \
tail +2 CHANGELOG.md >> CHANGELOG.md.new; \
tail -n +2 CHANGELOG.md >> CHANGELOG.md.new; \
perl -pi -e 's{VERSION = "$(CURVER)"}{VERSION = "$(NEWVER)"}' jira.go; \
mv CHANGELOG.md.new CHANGELOG.md; \
git commit -m "Updated Changelog" CHANGELOG.md; \
git tag $(NEWVER)
$(NULL)
release:
perl -pi -e 'undef $$/; s/\n```\nusage.*```//sg' README.md
echo '```' >> README.md
./jira --help-long >> README.md || true
echo '```' >> README.md
git diff --exit-code --quiet README.md || git commit -m "Updated Usage" README.md
git commit -m "Updated Changelog" CHANGELOG.md
git commit -m "version bump" jira.go
git tag v$(NEWVER)
git push --tags
version:
@echo $(CURVER)
clean:
rm -rf pkg dist bin src ./toolkit
rm -rf ./$(NAME)
prove:
chmod -R g-rwx,o-rwx $(CWD)/t/.gnupg
OSHT_VERBOSE=1 prove -v
generate:
cd schemas && ./fetch-schemas.py
grep -h slipscheme jiradata/*.go | grep json | sort | uniq | awk -F\/\/ '{print $$2}' | while read cmd; do $$cmd; done
+747 -139
View File
@@ -1,104 +1,100 @@
[![Join the chat at https://gitter.im/go-jira-cli/help](https://badges.gitter.im/go-jira-cli/help.svg)](https://gitter.im/go-jira-cli/help?utm_source=badge&utm_medium=badge&utm_content=badge)
[![Build Status](https://travis-ci.org/Netflix-Skunkworks/go-jira.svg?branch=master)](https://travis-ci.org/Netflix-Skunkworks/go-jira)
[![GoDoc](https://godoc.org/gopkg.in/Netflix-Skunkworks/go-jira.v1?status.png)](https://godoc.org/gopkg.in/Netflix-Skunkworks/go-jira.v1)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
# go-jira
simple command line client for Atlassian's Jira service written in Go
## Synopsis
## Install
```bash
jira ls -p GOJIRA # list all unresolved issues for project GOJRIA
jira ls -p GOJIRA -a mothra # as above also assigned to user mothra
jira ls -p GOJIRA -w mothra # lists GOJIRA unresolved issues watched by user mothra
jira ls -p GOJIRA -r mothra # list GOJIRA unresolved issues reported by user mothra
jira ls -t table -p GOJIRA # list all unresolved issues in pretty table output
jira view GOJIRA-321 # print Issue using "view" template
jira GOJIRA-321 # same as above
jira edit GOJIRA-321 # open up the issue in an editor, when you exit the
# editor the issue will post the updates to the server
# edit the issue, using the overirdes on the command line, skip the interactive editor:
jira edit GOJIRA-321 --noedit \
-o assignee=mothra \
-o comment="mothra, please take care of this." \
-o priority=Major
jira create -p GOJIRA # create new "Bug" type issue for project GOJIRA
jira create -p GOJIRA -i Task # create new Task type issue
jira trans close GOJIRA-321 # close issue, with interactive editor to set fields
jira close GOJIRA-321 --edit # same as above
# close the issue, set the resolution, and skip interactive editor:
jira trans close GOJIRA-321 -o resolution="Won't Fix" --noedit
# same as above
jira close GOJIRA-321 -o resolution="Won't Fix"
jira repopen GOJIRA-321 -m "reopening" # reopen issue
jira watch GOJIRA-321 # add self as watcher to the issue
jira comment GOJIRA-321 -m "done yet?" # add comment to the issue
jira take GOJIRA-321 # assign issue to self
jira give GOJIRA-321 mothra # assign issue to user mothra
# create local project config to set defaults
mkdir .jira.d
echo "project: GOJIRA" > .jira.d/config.yml
jira ls # list all unresolved issues for project GOJRIA
jira ls -a mothra # as above also assigned to user mothra
jira ls -w mothra # lists GOJIRA unresolved issues watched by user mothra
jira ls -r mothra # list GOJIRA unresolved issues reported by user mothra
jira ls -t table # list all unresolved issues in pretty table output
jira create # create new "Bug" type issue for project GOJIRA
jira create -i Task # create new Task type issue
# make the table template your default "list" template:
jira export-templates -t table
mv $HOME/.jira.d/templates/table $HOME/.jira.d/templates/list
```
## Download
### Download
You can download one of the pre-built binaries for **go-jira** [here](https://github.com/Netflix-Skunkworks/go-jira/releases).
## Build
### Build
* **NOTE** You will need **`go-1.4.1`** minimum
* If you do not have a **GOPATH** setup, these are simple build steps:
```bash
git clone git@github.com:Netflix-Skunkworks/go-jira.git
cd go-jira
export GOPATH=$(pwd)
export GOBIN=$GOPATH/bin
export PATH=$GOBIN:$PATH
cd src/github.com/Netflix-Skunkworks/go-jira/jira
go get -v
```
* If you do have a **GOPATH** setup, these are the standard steps to build:
You can build and install with [Go](https://golang.org/dl/):
```
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
go get gopkg.in/Netflix-Skunkworks/go-jira.v1/cmd/jira
```
## v1 vs v0 changes
###### **Golang library import**
For the new version of go-jira you should use:
```
import "gopkg.in/Netflix-Skunkworks/go-jira.v1"
```
If you have code that depends on the old apis, you can still use them with this import:
```
import "gopkg.in/Netflix-Skunkworks/go-jira.v0"
```
###### **Configs per command**
Instead of requiring a exectuable template to get configs for a given command now you can create a config to be applied to a command. So if you want to use `template: table` by default for yor `jira list` you can now do:
```
$ cat $HOME/.jira.d/list.yml
template: table
```
Where previously you needed something like:
```
# cat $HOME/.jira.d/config.yml
#!/bin/sh
case $JIRA_OPERATION in
list)
echo "template: table";;
esac
```
###### **Custom Commands**
Now you can create your own custom commands to do common operations with jira. Please see the details **Custom Commands** section below for more details. If you want to create a command `jira mine` that lists all the issues assigned to you now you can modify your `.jira.d/config.yml` file to add a `custom-commands` section like this:
```
custom-commands:
- name: mine
help: display issues assigned to me
script: |-
{{jira}} list --query "resolution = unresolved and assignee=currentuser() ORDER BY created"
```
Then the next time you run `jira help` you will see your usage:
```
$ jira mine --help
usage: jira mine
display issues assigned to me
Flags:
--help Show context-sensitive help (also try --help-long and --help-man).
-v, --verbose ... Increase verbosity for debugging
-e, --endpoint=ENDPOINT Base URI to use for Jira
-u, --user=USER Login name used for authentication with Jira service
--unixproxy=UNIXPROXY Path for a unix-socket proxy
-k, --insecure Disable TLS certificate verification
```
###### **Incompatible command changes**
Unfortunately during the rewrite between v0 and v1 there were some changes necessary that broke backwards compatibility with existing commands. Specifically the `dups`, `blocks`, `add worklog` and `add|remove|set labels` commands have had the command word swapped around:
* `jira DUPLICATE dups ISSUE` => `jira dup DUPLICATE ISSUE`
* `jira BLOCKER blocks ISSUE` => `jira block BLOCKER ISSUE`
* `jira add worklog` => `jira worklog add`
* `jira add labels` => `jira labels add`
* `jira remove labels` => `jira labels remove`
* `jira set labels` => `jira labels set`
###### **Login process change**
Previously `jira` used attempt to get a `JSESSION` cookies by authenticating with the webservice standard GUI login process. This has been especially problematic as users need to authenticate with various credential providers (google auth, etc). We now attempt to authenticate via the [session login api](https://docs.atlassian.com/jira/REST/cloud/#auth/1/session-login). This may be problematic for users if admins have locked down the session-login api, so we might have to bring back the error-prone Basic-Auth approach. For users that are unable to authenticate via `jira` hopefully someone in your organization can provide me with details on a process for you to authenticate and we can try to update `jira`.
## Configuration
**go-jira** uses a configuration hierarchy. When loading the configuration from disk it will recursively look through
all parent directories in your current path looking for a **.jira.d** directory. If your current directory is not
a child directory of your homedir, then your homedir will also be inspected for a **.jira.d** directory. From all of **.jira.d** directories
discovered **go-jira** will load a **config.yml** if found. The configuration properties found in a file closests to your current working directory
discovered **go-jira** will load a **<command>.yml** file (ie for `jira list` it will load `.jira.d/list.yml`) then it will merge in any properties from the **config.yml** if found. The configuration properties found in a file closests to your current working directory
will have precedence. Properties overriden with command line options will have final precedence.
The complicated configuration heirarchy is used because **go-jira** attempts to be context aware. For example, if you are working on a "foo" project and
The complicated configuration hierarchy is used because **go-jira** attempts to be context aware. For example, if you are working on a "foo" project and
you `cd` into your project workspace, wouldn't it be nice if `jira ls` automatically knew to list only issues related to the "foo" project? Likewise when you
`cd` to the "bar" project then `jira ls` should only list issues related to "bar" project. You can do this with by creating a configuration under your project
workspace at **./.jira.d/config.yml** that looks like:
@@ -117,6 +113,8 @@ endpoint: https://jira.mycompany.com
EOM
```
Then use `jira login` to authenticate yourself as $USER. To change your username, use the `-u` CLI flag or set `user:` in your config.yml
### 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:
@@ -147,6 +145,155 @@ case $JIRA_OPERATION in
esac
```
### Custom Commands
You can now create custom commands for `jira` just by editing your `.jira.d/config.yml` config file. These commands are effectively shell-scripts that can have documented options and arguments. The basic format is like:
```
custom-commands:
- command1
- command2
```
##### Commands
Where the individual commands are maps with these keys:
* `name: string` [**required**] This is the command name, so for `jira foobar` you would have `name: foobar`
* `help: string` This is help message displayed in the usage for the command
* `hidden: bool` This command will be hidden from users, but still executable. Sometimes useful for constructing complex commands where one custom command might call another.
* `default: bool` Use this for compound command groups. If you wanted to have `jira foo bar` and `jira foo baz` you would have two commands with `name: foo bar` and `name: foo baz`. Then if you wanted `jira foo baz` to be called by default when you type `jira foo` you would set `default: true` for that custom command.
* `options: list` This is the list of possible option flags that the command will accept
* `args: list` This is the list of command arguments (like the ISSUE) that the command will accept.
* `aliases: string list`: This is a list of alternate names that the user can provide on the command line to run the same command. Typically used to shorten the command name or provide alternatives that users might expect.
* `script: string` [**required**] This is the script that will be executed as the action for this command. The value will be treated as a template and substitutions for options and arguments will be made before executing.
##### Options
These are possible keys under the command `options` property:
* `name: string` [**required**] Name of the option, so `name: foobar` will result in `--foobar` option.
* `help: string` The help messsage displayed in usage for the option.
* `type: string`: The type of the option, can be one of these values: `BOOL`, `COUNTER`, `ENUM`, `FLOAT32`, `FLOAT64`, `INT8`, `INT16`, `INT32`, `INT64`, `INT`, `STRING`, `STRINGMAP`, `UINT8`, `UINT16`, `UINT32`, `UINT64` and `UINT`. Most of these are primitive data types an should be self-explanitory. The default type is `STRING`. There are some special types:
* `COUNTER` will be an integer type that increments each time the option is used. So something like `--count --count` will results in `{{options.count}}` of `2`.
* `ENUM` type is used with the `enum` property. The raw type is a string and **must** be one of the values listed in the `enum` property.
* `STRINGMAP` is a `string => string` map with the format of `KEY=VALUE`. So `--override foo=bar --override bin=baz` will allow for `{{options.override.foo}}` to be `bar` and `{{options.override.bin}}` to be `baz`.
* `short: char` The single character option to be used so `short: c` will allow for `-c`.
* `required: bool` Indicate that this option must be provided on the command line. Conflicts with the `default` property.
* `default: any` Specify the default value for the option. Conflicts with the `required` property.
* `hidden: bool` Hide the option from the usage help message, but otherwise works fine. Sometimes useful for developer options that user should not play with.
* `repeat: bool` Indicate that this option can be repeated. Not applicable for `COUNTER` and `STRINGMAP` types. This will turn the option value into an array that you can iterate over. So `--day Monday --day Thursday` can be used like `{{range options.day}}Day: {{.}}{{end}}`
* `enum: string list` Used with the `type: ENUM` property, it is a list of strings values that represent the set of possible values the option accepts.
##### Arguments
These are possible keys under the command `args` property:
* `name: string` [**required**] Name of the option, so `name: ISSUE` will show in the usasge as `jira <command> ISSUE`. This also represents the name of the argument to be used in the script template, so `{{args.ISSUE}}`.
* `help: string` The help messsage displayed in usage for the argument.
* `type: string`: The type of the argumemnt, can be one of these values: `BOOL`, `COUNTER`, `ENUM`, `FLOAT32`, `FLOAT64`, `INT8`, `INT16`, `INT32`, `INT64`, `INT`, `STRING`, `STRINGMAP`, `UINT8`, `UINT16`, `UINT32`, `UINT64` and `UINT`. Most of these are primitive data types an should be self-explanitory. The default type is `STRING`. There are some special types:
* `COUNTER` will be an integer type that increments each the argument is provided So something like `jira <command> ISSUE-12 ISSUE-23` will results in `{{args.ISSUE}}` of `2`.
* `ENUM` type is used with the `enum` property. The raw type is a string and **must** be one of the values listed in the `enum` property.
* `STRINGMAP` is a `string => string` map with the format of `KEY=VALUE`. So `jira <command> foo=bar bin=baz` along with a `name: OVERRIDE` property will allow for `{{args.OVERRIDE.foo}}` to be `bar` and `{{args.OVERRIDE.bin}}` to be `baz`.
* `required: bool` Indicate that this argument must be provided on the command line. Conflicts with the `default` property.
* `default: any` Specify the default value for the argument. Conflicts with the `required` property.
* `repeat: bool` Indicate that this argument can be repeated. Not applicable for `COUNTER` and `STRINGMAP` types. This will turn the template value into an array that you can iterate over. So `jira <command> ISSUE-12 ISSUE-23` can be used like `{{range args.ISSUE}}Issue: {{.}}{{end}}`
* `enum: string list` Used with the `type: ENUM` property, it is a list of strings values that represent the set of possible values for the argument.
##### Script Template
The `script` property is a template that whould produce `/bin/sh` compatible syntax after the template has been processed. There are 2 key template functions `{{args}}` and `{{options}}` that return the parsed arguments and option flags as a map.
To demonstrate how you might use args and options here is a `custom-test` command:
```
custom-commands:
- name: custom-test
help: Testing the custom commands
options:
- name: abc
short: a
default: default
- name: day
type: ENUM
enum:
- Monday
- Tuesday
- Wednesday
- Thursday
- Friday
required: true
args:
- name: ARG
required: true
- name: MORE
repeat: true
script: |
echo COMMAND {{args.ARG}} --abc {{options.abc}} --day {{options.day}} {{range $more := args.MORE}}{{$more}} {{end}}
```
Then to run it:
```
$ jira custom-test
ERROR Invalid Usage: required flag --day not provided
$ jira custom-test --day Sunday
ERROR Invalid Usage: enum value must be one of Monday,Tuesday,Wednesday,Thursday,Friday, got 'Sunday'
$ jira custom-test --day Tuesday
ERROR Invalid Usage: required argument 'ARG' not provided
$ jira custom-test --day Tuesday arg1
COMMAND arg1 --abc default --day Tuesday
$ jira custom-test --day Tuesday arg1 more1 more2 more3
COMMAND arg1 --abc default --day Tuesday more1 more2 more3
$ jira custom-test --day Tuesday arg1 more1 more2 more3 --abc non-default
COMMAND arg1 --abc non-default --day Tuesday more1 more2 more3
$ jira custom-test --day Tuesday arg1 more1 more2 more3 -a short-non-default
COMMAND arg1 --abc short-non-default --day Tuesday more1 more2 more3
```
The script has access to all the environment variables that are in your current environment plus those that `jira` will set. `jira` sets environment variables for each config property it has parsed from `.jira.d/config.yml` or the command configs at `.jira.d/<command>.yml`. It might be useful to see all environment variables that `jira` is producing, so here is a simple custom command to list them:
```
custom-commands:
- name: env
help: print the JIRA environment variables available to custom commands
script: |
env | grep JIRA
```
You could use the environment variables automatically, so if your `.jira.d/config.yml` looks something like this:
```
project: PROJECT
custom-commands:
- name: print-project
help: print the name of the configured project
script: "echo $JIRA_PROJECT"
```
##### Examples
* `jira mine` for listing issues assigned to you
```
custom-commands:
- name: mine
help: display issues assigned to me
script: |-
if [ -n "$JIRA_PROJECT" ]; then
# if `project: ...` configured just list the issues for current project
{{jira}} list --template table --query "resolution = unresolved and assignee=currentuser() and project = $JIRA_PROJECT ORDER BY priority asc, created"
else
# otherwise list issues for all project
{{jira}} list --template table --query "resolution = unresolved and assignee=currentuser() ORDER BY priority asc, created"
fi
```
* `jira sprint` for listing issues in your current sprint
```
custom-commands:
- name: sprint
help: display issues for active sprint
script: |-
if [ -n "$JIRA_PROJECT" ]; then
# if `project: ...` configured just list the issues for current project
{{jira}} list --template table --query "sprint in openSprints() and type != epic and resolution = unresolved and project=$JIRA_PROJECT ORDER BY rank asc, created"
else
# otherwise list issues for all project
echo "\"project: ...\" configuration missing from .jira.d/config.yml"
fi
```
### Editing
When you run command like `jira edit` it will open up your favorite editor with the templatized output so you can quickly edit. When the editor
@@ -167,69 +314,530 @@ 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/jiracli/templates.go#L64).
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
```
### Authentication
By default `go-jira` will prompt for a password automatically when get a response header from the Jira service that indicates you do not have an active session (ie the `X-Ausername` header is set to `anonymous`). Then after authentication we cache the `cloud.session.token` cookie returned by the service [session login api](https://docs.atlassian.com/jira/REST/cloud/#auth/1/session-login) and reuse that on subsequent requests. Typically this cookie will be valid for several hours (depending on the service configuration). To automatically securely store your password for easy reuse by jira You can enable a `password-source` via `.jira.d/config.yml` with possible values of `keyring` or `pass`.
#### keyring password source
On OSX and Linux there are a few keyring providers that `go-jira` can use (via this [golang module](https://github.com/tmc/keyring)). To integrate `go-jira` with a supported keyring just add this configuration to `$HOME/.jira.d/config.yml`:
```yaml
password-source: keyring
```
After setting this and issuing a `jira login`, your credentials will be stored in your platform's backend (e.g. Keychain for Mac OS X) automatically. Subsequent operations, like a `jira ls`, should automatically login.
#### `pass` password source
An alternative to the keyring password source is the `pass` tool (documentation [here](https://www.passwordstore.org/)). This uses gpg to encrypt/decrypt passwords on demand and by using `gpg-agent` you can cache the gpg credentials for a period of time so you will not be prompted repeatedly for decrypting the passwords. The advantage over the keyring integration is that `pass` can be used on more platforms than OSX and Linux, although it does require more setup. To use `pass` for password storage and retrieval via `go-jira` just add this configuration to `$HOME/.jira.d/config.yml`:
```yaml
password-source: pass
```
This assumes you have already setup `pass` correctly on your system. Specifically you will need to have created a gpg key like this:
```
$ gpg --gen-key
```
Then you will need the GPG Key ID you want associated with `pass`. First list the available keys:
```
$ gpg --list-keys
/home/gojira/.gnupg/pubring.gpg
-------------------------------------------------
pub 2048R/A307D709 2016-12-18
uid Go Jira <gojira@example.com>
sub 2048R/F9A047B8 2016-12-18
```
Then initialize the `pass` tool to use the correct key:
```
$ pass init "Go Jira <gojira@example.com>"
```
You probably want to setup gpg-agent so that you dont have to type in your gpg passphrase all the time. You can get `gpg-agent` to automatically start by adding something like this to your `$HOME/.bashrc`
```bash
if [ -f $HOME/.gpg-agent-info ]; then
. $HOME/.gpg-agent-info
export GPG_AGENT_INFO
fi
if [ ! -f $HOME/.gpg-agent.conf ]; then
cat <<EOM >$HOME/.gpg-agent.conf
default-cache-ttl 604800
max-cache-ttl 604800
default-cache-ttl-ssh 604800
max-cache-ttl-ssh 604800
EOM
fi
if [ -n "${GPG_AGENT_INFO}" ]; then
nc -U "${GPG_AGENT_INFO%%:*}" >/dev/null </dev/null
if [ ! -S "${GPG_AGENT_INFO%%:*}" -o $? != 0 ]; then
# set passphrase cache so I only have to type my passphrase once a day
eval $(gpg-agent --options $HOME/.gpg-agent.conf --daemon --write-env-file $HOME/.gpg-agent-info --use-standard-socket --log-file $HOME/tmp/gpg-agent.log --verbose)
fi
fi
export GPG_TTY=$(tty)
```
## Usage
```
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 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
usage: jira [<flags>] <command> [<args> ...]
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: $USER)
-v --verbose Increase output logging
Jira Command Line Interface
Query Options:
-a --assignee=USER Username assigned the issue
-c --component=COMPONENT Component to Search for
-f --queryfields=FIELDS Fields that are used in "list" template: (default: summary,created,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
-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
Flags:
--help Show context-sensitive help (also try --help-long and --help-man).
-v, --verbose ... Increase verbosity for debugging
-e, --endpoint=ENDPOINT Base URI to use for Jira
-k, --insecure Disable TLS certificate verification
-Q, --quiet Suppress output to console
--unixproxy=UNIXPROXY Path for a unix-socket proxy
-u, --user=USER Login name used for authentication with Jira service
Edit Options:
-m --comment=COMMENT Comment message for transition
-o --override=KEY=VAL Set custom key/value pairs
Commands:
help [<command>...]
Show help.
version
Prints version
login
Attempt to login into jira server
logout
Deactivate sesssion with Jira server
list [<flags>]
Prints list of issues for given search criteria
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
-a, --assignee=ASSIGNEE User assigned the issue
-c, --component=COMPONENT Component to search for
-i, --issuetype=ISSUETYPE Issue type to search for
-l, --limit=LIMIT Maximum number of results to return in search
-p, --project=PROJECT Project to search for
-n, --named-query=NAMED-QUERY The name of a query in the `queries` configuration
-q, --query=QUERY Jira Query Language (JQL) expression for the search
-f, --queryfields=QUERYFIELDS Fields that are used in "list" template
-r, --reporter=REPORTER Reporter to search for
-S, --status=STATUS Filter on issue status
-s, --sort=SORT Sort order to return
-w, --watcher=WATCHER Watcher to search for
view [<flags>] <ISSUE>
Prints issue details
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
--expand=EXPAND ... field to expand for the issue
--field=FIELD ... field to return for the issue
--property=PROPERTY ... property to return for issue
create [<flags>]
Create issue
-b, --browse Open issue(s) in browser after operation
--editor=EDITOR Editor to use
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-p, --project=PROJECT project to create issue in
-i, --issuetype=ISSUETYPE issuetype in to create
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
--saveFile=SAVEFILE Write issue as yaml to file
edit [<flags>] [<ISSUE>]
Edit issue details
-b, --browse Open issue(s) in browser after operation
--editor=EDITOR Editor to use
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-n, --named-query=NAMED-QUERY The name of a query in the `queries` configuration
-q, --query=QUERY Jira Query Language (JQL) expression for the search to edit multiple issues
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
comment [<flags>] [<ISSUE>]
Add comment to issue
-b, --browse Open issue(s) in browser after operation
--editor=EDITOR Editor to use
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
epic create [<flags>]
Create Epic
-b, --browse Open issue(s) in browser after operation
--editor=EDITOR Editor to use
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-p, --project=PROJECT project to create epic in
-n, --epic-name=EPIC-NAME Epic Name
-m, --comment=COMMENT Comment message for epic
-o, --override=OVERRIDE ... Set epic property
--saveFile=SAVEFILE Write epic as yaml to file
epic list [<flags>] <EPIC>
Prints list of issues for an epic with optional search criteria
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
-a, --assignee=ASSIGNEE User assigned the issue
-c, --component=COMPONENT Component to search for
-i, --issuetype=ISSUETYPE Issue type to search for
-l, --limit=LIMIT Maximum number of results to return in search
-p, --project=PROJECT Project to search for
-n, --named-query=NAMED-QUERY The name of a query in the `queries` configuration
-q, --query=QUERY Jira Query Language (JQL) expression for the search
-f, --queryfields=QUERYFIELDS Fields that are used in "list" template
-r, --reporter=REPORTER Reporter to search for
-S, --status=STATUS Filter on issue status
-s, --sort=SORT Sort order to return
-w, --watcher=WATCHER Watcher to search for
epic add <EPIC> <ISSUE>...
Add issues to Epic
epic remove <ISSUE>...
Remove issues from Epic
worklog list [<flags>] <ISSUE>
Prints the worklog data for given issue
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
worklog add [<flags>] <ISSUE>
Add a worklog to an issue
-b, --browse Open issue(s) in browser after operation
--editor=EDITOR Editor to use
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for worklog
-T, --time-spent=TIME-SPENT Time spent working on issue
-S, --started=STARTED Time you started work
fields [<flags>]
Prints all fields, both System and Custom
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
createmeta [<flags>]
View 'create' metadata
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
-p, --project=PROJECT project to fetch create metadata
-i, --issuetype=ISSUETYPE issuetype in project to fetch create metadata
editmeta [<flags>] <ISSUE>
View 'edit' metadata
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
subtask [<flags>] [<ISSUE>]
Subtask issue
-b, --browse Open issue(s) in browser after operation
--editor=EDITOR Editor to use
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-p, --project=PROJECT project to subtask issue in
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
dup [<flags>] <DUPLICATE> <ISSUE>
Mark issues as duplicate
-b, --browse Open issue(s) in browser after operation
--editor=EDITOR Editor to use
-t, --template=TEMPLATE Template to use for output
-m, --comment=COMMENT Comment message when marking issue as duplicate
block [<flags>] <BLOCKER> <ISSUE>
Mark issues as blocker
-b, --browse Open issue(s) in browser after operation
--editor=EDITOR Editor to use
-t, --template=TEMPLATE Template to use for output
-m, --comment=COMMENT Comment message when marking issue as blocker
issuelink [<flags>] <OUTWARDISSUE> <ISSUELINKTYPE> <INWARDISSUE>
Link two issues
-b, --browse Open issue(s) in browser after operation
--editor=EDITOR Editor to use
-t, --template=TEMPLATE Template to use for output
-m, --comment=COMMENT Comment message when linking issue
issuelinktypes [<flags>]
Show the issue link types
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
transition [<flags>] <TRANSITION> <ISSUE>
Transition issue to given state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
transitions [<flags>] <ISSUE>
List valid issue transitions
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
transmeta [<flags>] <ISSUE>
List valid issue transitions
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
close [<flags>] <ISSUE>
Transition issue to close state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
acknowledge [<flags>] <ISSUE>
Transition issue to acknowledge state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
reopen [<flags>] <ISSUE>
Transition issue to reopen state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
resolve [<flags>] <ISSUE>
Transition issue to resolve state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
start [<flags>] <ISSUE>
Transition issue to start state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
stop [<flags>] <ISSUE>
Transition issue to stop state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
todo [<flags>] <ISSUE>
Transition issue to To Do state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
backlog [<flags>] <ISSUE>
Transition issue to Backlog state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
done [<flags>] <ISSUE>
Transition issue to Done state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
in-progress [<flags>] <ISSUE>
Transition issue to Progress state
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-m, --comment=COMMENT Comment message for issue
-o, --override=OVERRIDE ... Set issue property
vote [<flags>] [<ISSUE>]
Vote up/down an issue
-b, --browse Open issue(s) in browser after operation
-d, --down downvote the issue
rank [<flags>] <FIRST-ISSUE> <after|before> <SECOND-ISSUE>
Mark issues as blocker
-b, --browse Open issue(s) in browser after operation
watch [<flags>] <ISSUE> [<WATCHER>]
Add/Remove watcher to issue
-b, --browse Open issue(s) in browser after operation
-r, --remove remove watcher from issue
labels add [<flags>] <ISSUE> <LABEL>...
Add labels to an issue
-b, --browse Open issue(s) in browser after operation
labels set [<flags>] <ISSUE> <LABEL>...
Set labels on an issue
-b, --browse Open issue(s) in browser after operation
labels remove [<flags>] <ISSUE> <LABEL>...
Remove labels from an issue
-b, --browse Open issue(s) in browser after operation
take [<flags>] <ISSUE> [<ASSIGNEE>]
Assign issue to yourself
-b, --browse Open issue(s) in browser after operation
--default use default user for assignee
assign [<flags>] <ISSUE> [<ASSIGNEE>]
Assign user to issue
-b, --browse Open issue(s) in browser after operation
--default use default user for assignee
unassign [<flags>] <ISSUE> [<ASSIGNEE>]
Unassign an issue
-b, --browse Open issue(s) in browser after operation
--default use default user for assignee
component add [<flags>]
Add component
--editor=EDITOR Editor to use
-t, --template=TEMPLATE Template to use for output
--noedit Disable opening the editor
-p, --project=PROJECT project to create component in
-n, --name=NAME name of component
-d, --description=DESCRIPTION description of component
-l, --lead=LEAD person that acts as lead for component
components [<flags>]
Show components for a project
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
-p, --project=PROJECT project to list components
issuetypes [<flags>]
Show issue types for a project
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
-p, --project=PROJECT project to list issueTypes
attach create [<flags>] <ISSUE> [<ATTACHMENT>]
Attach file to issue
-b, --browse Open issue(s) in browser after operation
--saveFile=SAVEFILE Write attachment information as yaml to file
-f, --filename=FILENAME Filename to use for attachment
attach list [<flags>] <ISSUE>
Prints issue details
-b, --browse Open issue(s) in browser after operation
-t, --template=TEMPLATE Template to use for output
attach get [<flags>] [<ATTACHMENT-ID>]
Fetch attachment
-o, --output=OUTPUT Write attachment to specified file name, '-' for stdout
attach remove [<ATTACHMENT-ID>]
Delete attachment
export-templates [<flags>]
Export templates for customizations
-t, --template=TEMPLATE Template to export
-d, --dir=DIR directory to write tempates to
unexport-templates [<flags>]
Remove unmodified exported templates
-t, --template=TEMPLATE Template to export
-d, --dir=DIR directory to write tempates to
browse <ISSUE>
Open issue in browser
request [<flags>] <API> [<JSON>]
Open issue in requestr
-t, --template=TEMPLATE Template to use for output
--gjq=GJQ GJSON Query to filter output, see https://goo.gl/iaYwJ5
-M, --method=METHOD HTTP request method to use
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)
```
+46
View File
@@ -0,0 +1,46 @@
package jira
import (
"fmt"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
)
// https://docs.atlassian.com/jira/REST/cloud/#api/2/attachment-getAttachment
func (j *Jira) GetAttachment(id string) (*jiradata.Attachment, error) {
return GetAttachment(j.UA, j.Endpoint, id)
}
func GetAttachment(ua HttpClient, endpoint string, id string) (*jiradata.Attachment, error) {
uri := fmt.Sprintf("%s/rest/api/2/attachment/%s", endpoint, id)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.Attachment{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/attachment-removeAttachment
func (j *Jira) RemoveAttachment(id string) error {
return RemoveAttachment(j.UA, j.Endpoint, id)
}
func RemoveAttachment(ua HttpClient, endpoint string, id string) error {
uri := fmt.Sprintf("%s/rest/api/2/attachment/%s", endpoint, id)
resp, err := ua.Delete(uri)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
+410
View File
@@ -0,0 +1,410 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"runtime/debug"
"strconv"
"syscall"
"github.com/coryb/figtree"
"github.com/coryb/kingpeon"
"github.com/coryb/oreo"
jira "gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracmd"
kingpin "gopkg.in/alecthomas/kingpin.v2"
"gopkg.in/op/go-logging.v1"
)
var (
log = logging.MustGetLogger("jira")
defaultFormat = func() string {
format := os.Getenv("JIRA_LOG_FORMAT")
if format != "" {
return format
}
return "%{color}%{level:-5s}%{color:reset} %{message}"
}()
)
func handleExit() {
if e := recover(); e != nil {
if exit, ok := e.(jiracli.Exit); ok {
os.Exit(exit.Code)
} else {
fmt.Fprintf(os.Stderr, "%s\n%s", e, debug.Stack())
os.Exit(1)
}
}
}
func increaseLogLevel(verbosity int) {
logging.SetLevel(logging.GetLevel("")+logging.Level(verbosity), "")
if logging.GetLevel("") > logging.DEBUG {
oreo.TraceRequestBody = true
oreo.TraceResponseBody = true
}
}
func main() {
defer handleExit()
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
format := os.Getenv("JIRA_LOG_FORMAT")
if format == "" {
format = defaultFormat
}
logging.SetBackend(
logging.NewBackendFormatter(
logBackend,
logging.MustStringFormatter(format),
),
)
if os.Getenv("JIRA_DEBUG") == "" {
logging.SetLevel(logging.NOTICE, "")
} else {
logging.SetLevel(logging.DEBUG, "")
}
app := kingpin.New("jira", "Jira Command Line Interface")
app.Command("version", "Prints version").PreAction(func(*kingpin.ParseContext) error {
fmt.Println(jira.VERSION)
panic(jiracli.Exit{Code: 0})
})
var verbosity int
app.Flag("verbose", "Increase verbosity for debugging").Short('v').PreAction(func(_ *kingpin.ParseContext) error {
os.Setenv("JIRA_DEBUG", fmt.Sprintf("%d", verbosity))
increaseLogLevel(1)
return nil
}).CounterVar(&verbosity)
if os.Getenv("JIRA_DEBUG") != "" {
if verbosity, err := strconv.Atoi(os.Getenv("JIRA_DEBUG")); err == nil {
increaseLogLevel(verbosity)
}
}
fig := figtree.NewFigTree()
fig.EnvPrefix = "JIRA"
fig.ConfigDir = ".jira.d"
if err := os.MkdirAll(filepath.Join(jiracli.Homedir(), fig.ConfigDir), 0755); err != nil {
log.Errorf("%s", err)
panic(jiracli.Exit{Code: 1})
}
o := oreo.New().WithCookieFile(filepath.Join(jiracli.Homedir(), fig.ConfigDir, "cookies.js"))
registry := []jiracli.CommandRegistry{
jiracli.CommandRegistry{
Command: "login",
Entry: jiracmd.CmdLoginRegistry(),
},
jiracli.CommandRegistry{
Command: "logout",
Entry: jiracmd.CmdLogoutRegistry(),
},
jiracli.CommandRegistry{
Command: "list",
Aliases: []string{"ls"},
Entry: jiracmd.CmdListRegistry(),
},
jiracli.CommandRegistry{
Command: "view",
Entry: jiracmd.CmdViewRegistry(),
},
jiracli.CommandRegistry{
Command: "create",
Entry: jiracmd.CmdCreateRegistry(),
},
jiracli.CommandRegistry{
Command: "edit",
Entry: jiracmd.CmdEditRegistry(),
},
jiracli.CommandRegistry{
Command: "comment",
Entry: jiracmd.CmdCommentRegistry(),
},
jiracli.CommandRegistry{
Command: "epic create",
Entry: jiracmd.CmdEpicCreateRegistry(),
},
jiracli.CommandRegistry{
Command: "epic list",
Entry: jiracmd.CmdEpicListRegistry(),
Aliases: []string{"ls"},
},
jiracli.CommandRegistry{
Command: "epic add",
Entry: jiracmd.CmdEpicAddRegistry(),
},
jiracli.CommandRegistry{
Command: "epic remove",
Entry: jiracmd.CmdEpicRemoveRegistry(),
Aliases: []string{"rm"},
},
jiracli.CommandRegistry{
Command: "worklog list",
Entry: jiracmd.CmdWorklogListRegistry(),
Default: true,
},
jiracli.CommandRegistry{
Command: "worklog add",
Entry: jiracmd.CmdWorklogAddRegistry(),
},
jiracli.CommandRegistry{
Command: "fields",
Entry: jiracmd.CmdFieldsRegistry(),
},
jiracli.CommandRegistry{
Command: "createmeta",
Entry: jiracmd.CmdCreateMetaRegistry(),
},
jiracli.CommandRegistry{
Command: "editmeta",
Entry: jiracmd.CmdEditMetaRegistry(),
},
jiracli.CommandRegistry{
Command: "subtask",
Entry: jiracmd.CmdSubtaskRegistry(),
},
jiracli.CommandRegistry{
Command: "dup",
Entry: jiracmd.CmdDupRegistry(),
},
jiracli.CommandRegistry{
Command: "block",
Entry: jiracmd.CmdBlockRegistry(),
},
jiracli.CommandRegistry{
Command: "issuelink",
Entry: jiracmd.CmdIssueLinkRegistry(),
},
jiracli.CommandRegistry{
Command: "issuelinktypes",
Entry: jiracmd.CmdIssueLinkTypesRegistry(),
},
jiracli.CommandRegistry{
Command: "transition",
Aliases: []string{"trans"},
Entry: jiracmd.CmdTransitionRegistry(""),
},
jiracli.CommandRegistry{
Command: "transitions",
Entry: jiracmd.CmdTransitionsRegistry("transitions"),
},
jiracli.CommandRegistry{
Command: "transmeta",
Entry: jiracmd.CmdTransitionsRegistry("debug"),
},
jiracli.CommandRegistry{
Command: "close",
Entry: jiracmd.CmdTransitionRegistry("close"),
},
jiracli.CommandRegistry{
Command: "acknowledge",
Aliases: []string{"ack"},
Entry: jiracmd.CmdTransitionRegistry("acknowledge"),
},
jiracli.CommandRegistry{
Command: "reopen",
Entry: jiracmd.CmdTransitionRegistry("reopen"),
},
jiracli.CommandRegistry{
Command: "resolve",
Entry: jiracmd.CmdTransitionRegistry("resolve"),
},
jiracli.CommandRegistry{
Command: "start",
Entry: jiracmd.CmdTransitionRegistry("start"),
},
jiracli.CommandRegistry{
Command: "stop",
Entry: jiracmd.CmdTransitionRegistry("stop"),
},
jiracli.CommandRegistry{
Command: "todo",
Entry: jiracmd.CmdTransitionRegistry("To Do"),
},
jiracli.CommandRegistry{
Command: "backlog",
Entry: jiracmd.CmdTransitionRegistry("Backlog"),
},
jiracli.CommandRegistry{
Command: "done",
Entry: jiracmd.CmdTransitionRegistry("Done"),
},
jiracli.CommandRegistry{
Command: "in-progress",
Aliases: []string{"prog", "progress"},
Entry: jiracmd.CmdTransitionRegistry("Progress"),
},
jiracli.CommandRegistry{
Command: "vote",
Entry: jiracmd.CmdVoteRegistry(),
},
jiracli.CommandRegistry{
Command: "rank",
Entry: jiracmd.CmdRankRegistry(),
},
jiracli.CommandRegistry{
Command: "watch",
Entry: jiracmd.CmdWatchRegistry(),
},
jiracli.CommandRegistry{
Command: "labels add",
Entry: jiracmd.CmdLabelsAddRegistry(),
},
jiracli.CommandRegistry{
Command: "labels set",
Entry: jiracmd.CmdLabelsSetRegistry(),
},
jiracli.CommandRegistry{
Command: "labels remove",
Entry: jiracmd.CmdLabelsRemoveRegistry(),
Aliases: []string{"rm"},
},
jiracli.CommandRegistry{
Command: "take",
Entry: jiracmd.CmdTakeRegistry(),
},
jiracli.CommandRegistry{
Command: "assign",
Entry: jiracmd.CmdAssignRegistry(),
Aliases: []string{"give"},
},
jiracli.CommandRegistry{
Command: "unassign",
Entry: jiracmd.CmdUnassignRegistry(),
},
jiracli.CommandRegistry{
Command: "component add",
Entry: jiracmd.CmdComponentAddRegistry(),
},
jiracli.CommandRegistry{
Command: "components",
Entry: jiracmd.CmdComponentsRegistry(),
},
jiracli.CommandRegistry{
Command: "issuetypes",
Entry: jiracmd.CmdIssueTypesRegistry(),
},
jiracli.CommandRegistry{
Command: "attach create",
Entry: jiracmd.CmdAttachCreateRegistry(),
},
jiracli.CommandRegistry{
Command: "attach list",
Entry: jiracmd.CmdAttachListRegistry(),
Aliases: []string{"ls"},
},
jiracli.CommandRegistry{
Command: "attach get",
Entry: jiracmd.CmdAttachGetRegistry(),
},
jiracli.CommandRegistry{
Command: "attach remove",
Entry: jiracmd.CmdAttachRemoveRegistry(),
Aliases: []string{"rm"},
},
jiracli.CommandRegistry{
Command: "export-templates",
Entry: jiracmd.CmdExportTemplatesRegistry(),
},
jiracli.CommandRegistry{
Command: "unexport-templates",
Entry: jiracmd.CmdUnexportTemplatesRegistry(),
},
jiracli.CommandRegistry{
Command: "browse",
Entry: jiracmd.CmdBrowseRegistry(),
Aliases: []string{"b"},
},
jiracli.CommandRegistry{
Command: "request",
Entry: jiracmd.CmdRequestRegistry(),
Aliases: []string{"req"},
},
}
jiracli.Register(app, o, fig, registry)
// register custom commands
data := struct {
CustomCommands kingpeon.DynamicCommands `yaml:"custom-commands" json:"custom-commands"`
}{}
if err := fig.LoadAllConfigs("config.yml", &data); err != nil {
log.Errorf("%s", err)
panic(jiracli.Exit{Code: 1})
}
if len(data.CustomCommands) > 0 {
runner := syscall.Exec
if runtime.GOOS == "windows" {
runner = func(binary string, cmd []string, env []string) error {
command := exec.Command(binary, cmd[1:]...)
command.Stdin = os.Stdin
command.Stdout = os.Stdout
command.Stderr = os.Stderr
command.Env = env
return command.Run()
}
}
tmp := map[string]interface{}{}
fig.LoadAllConfigs("config.yml", &tmp)
kingpeon.RegisterDynamicCommandsWithRunner(runner, app, data.CustomCommands, jiracli.TemplateProcessor())
}
app.Terminate(func(status int) {
for _, arg := range os.Args {
if arg == "-h" || arg == "--help" || len(os.Args) == 1 {
panic(jiracli.Exit{Code: 0})
}
}
panic(jiracli.Exit{Code: 1})
})
// checking for default usage of `jira ISSUE-123` but need to allow
// for global options first like: `jira --user mothra ISSUE-123`
ctx, err := app.ParseContext(os.Args[1:])
if err != nil && ctx == nil {
// This is an internal kingpin usage error, duplicate options/commands
log.Fatalf("error: %s, ctx: %v", err, ctx)
}
if ctx != nil {
if ctx.SelectedCommand == nil {
next := ctx.Next()
if next != nil {
if ok, err := regexp.MatchString("^[A-Z]+-[0-9]+$", next.Value); err != nil {
log.Errorf("Invalid Regex: %s", err)
} else if ok {
// insert "view" at i=1 (2nd position)
os.Args = append(os.Args[:1], append([]string{"view"}, os.Args[1:]...)...)
}
}
}
}
if _, err := app.Parse(os.Args[1:]); err != nil {
if _, ok := err.(*jiracli.Error); ok {
log.Errorf("%s", err)
panic(jiracli.Exit{Code: 1})
} else {
ctx, _ := app.ParseContext(os.Args[1:])
if ctx != nil {
app.UsageForContext(ctx)
}
log.Errorf("Invalid Usage: %s", err)
panic(jiracli.Exit{Code: 1})
}
}
}
+38
View File
@@ -0,0 +1,38 @@
package jira
import (
"bytes"
"encoding/json"
"fmt"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
)
type ComponentProvider interface {
ProvideComponent() *jiradata.Component
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/component-createComponent
func (j *Jira) CreateComponent(cp ComponentProvider) (*jiradata.Component, error) {
return CreateComponent(j.UA, j.Endpoint, cp)
}
func CreateComponent(ua HttpClient, endpoint string, cp ComponentProvider) (*jiradata.Component, error) {
req := cp.ProvideComponent()
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s/rest/api/2/component", endpoint)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 201 {
results := &jiradata.Component{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
+113
View File
@@ -0,0 +1,113 @@
package jira
import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"strings"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
)
// https://docs.atlassian.com/jira-software/REST/latest/#agile/1.0/epic-getIssuesForEpic
func (j *Jira) EpicSearch(epic string, sp SearchProvider) (*jiradata.SearchResults, error) {
return EpicSearch(j.UA, j.Endpoint, epic, sp)
}
func EpicSearch(ua HttpClient, endpoint string, epic string, sp SearchProvider) (*jiradata.SearchResults, error) {
req := sp.ProvideSearchRequest()
// encoded, err := json.Marshal(req)
// if err != nil {
// return nil, err
// }
uri, err := url.Parse(fmt.Sprintf("%s/rest/agile/1.0/epic/%s/issue", endpoint, epic))
if err != nil {
return nil, err
}
params := url.Values{}
if len(req.Fields) > 0 {
params.Add("fields", strings.Join(req.Fields, ","))
}
if req.JQL != "" {
params.Add("jql", req.JQL)
}
if req.MaxResults != 0 {
params.Add("maxResults", fmt.Sprintf("%d", req.MaxResults))
}
if req.StartAt != 0 {
params.Add("startAt", fmt.Sprintf("%d", req.StartAt))
}
if req.ValidateQuery != "" {
params.Add("validateQuery", req.ValidateQuery)
}
uri.RawQuery = params.Encode()
resp, err := ua.Do(oreo.RequestBuilder(uri).WithHeader("Accept", "application/json").Build())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.SearchResults{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
type EpicIssuesProvider interface {
ProvideEpicIssues() *jiradata.EpicIssues
}
// https://docs.atlassian.com/jira-software/REST/latest/#agile/1.0/epic-moveIssuesToEpic
func (j *Jira) EpicAddIssues(epic string, eip EpicIssuesProvider) error {
return EpicAddIssues(j.UA, j.Endpoint, epic, eip)
}
func EpicAddIssues(ua HttpClient, endpoint string, epic string, eip EpicIssuesProvider) error {
req := eip.ProvideEpicIssues()
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/agile/1.0/epic/%s/issue", endpoint, epic)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira-software/REST/latest/#agile/1.0/epic-removeIssuesFromEpic
func (j *Jira) EpicRemoveIssues(eip EpicIssuesProvider) error {
return EpicRemoveIssues(j.UA, j.Endpoint, eip)
}
func EpicRemoveIssues(ua HttpClient, endpoint string, eip EpicIssuesProvider) error {
req := eip.ProvideEpicIssues()
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/agile/1.0/epic/none/issue", endpoint)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
+19
View File
@@ -0,0 +1,19 @@
package jira
import (
"fmt"
"net/http"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
)
func responseError(resp *http.Response) error {
results := &jiradata.ErrorCollection{}
if err := readJSON(resp.Body, results); err != nil {
return err
}
if len(results.ErrorMessages) == 0 && len(results.Errors) == 0 {
return fmt.Errorf(resp.Status)
}
return results
}
+26
View File
@@ -0,0 +1,26 @@
package jira
import (
"fmt"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
)
// https://docs.atlassian.com/jira/REST/cloud/#api/2/field-getFields
func (j *Jira) GetFields() ([]jiradata.Field, error) {
return GetFields(j.UA, j.Endpoint)
}
func GetFields(ua HttpClient, endpoint string) ([]jiradata.Field, error) {
uri := fmt.Sprintf("%s/rest/api/2/field", endpoint)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := []jiradata.Field{}
return results, readJSON(resp.Body, &results)
}
return nil, responseError(resp)
}
+14
View File
@@ -0,0 +1,14 @@
package jira
import (
"io"
"net/http"
)
type HttpClient interface {
Delete(url string) (*http.Response, error)
Do(*http.Request) (*http.Response, error)
GetJSON(url string) (*http.Response, error)
Post(url, bodyType string, body io.Reader) (*http.Response, error)
Put(url, bodyType string, body io.Reader) (*http.Response, error)
}
+587
View File
@@ -0,0 +1,587 @@
package jira
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/url"
"strings"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
)
type IssueQueryProvider interface {
ProvideIssueQueryString() string
}
type IssueOptions struct {
Fields []string `json:"fields,omitempty" yaml:"fields,omitempty"`
Expand []string `json:"expand,omitempty" yaml:"expand,omitempty"`
Properties []string `json:"properties,omitempty" yaml:"properties,omitempty"`
FieldsByKeys bool `json:"fieldsByKeys,omitempty" yaml:"fieldsByKeys,omitempty"`
UpdateHistory bool `json:"updateHistory,omitempty" yaml:"updateHistory,omitempty"`
}
func (o *IssueOptions) ProvideIssueQueryString() string {
params := []string{}
if len(o.Fields) > 0 {
params = append(params, "fields="+strings.Join(o.Fields, ","))
}
if len(o.Expand) > 0 {
params = append(params, "expand="+strings.Join(o.Expand, ","))
}
if len(o.Properties) > 0 {
params = append(params, "properties="+strings.Join(o.Properties, ","))
}
if o.FieldsByKeys {
params = append(params, "fieldsByKeys=true")
}
if o.UpdateHistory {
params = append(params, "updateHistory=true")
}
if len(params) > 0 {
return "?" + strings.Join(params, "&")
}
return ""
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getIssue
func (j *Jira) GetIssue(issue string, iqg IssueQueryProvider) (*jiradata.Issue, error) {
return GetIssue(j.UA, j.Endpoint, issue, iqg)
}
func GetIssue(ua HttpClient, endpoint string, issue string, iqg IssueQueryProvider) (*jiradata.Issue, error) {
query := ""
if iqg != nil {
query = iqg.ProvideIssueQueryString()
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s%s", endpoint, issue, query)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.Issue{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
func (j *Jira) GetIssueWorklog(issue string) (*jiradata.Worklogs, error) {
return GetIssueWorklog(j.UA, j.Endpoint, issue)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/worklog-getIssueWorklog
func GetIssueWorklog(ua HttpClient, endpoint string, issue string) (*jiradata.Worklogs, error) {
startAt := 0
total := 1
maxResults := 100
worklogs := jiradata.Worklogs{}
for startAt < total {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/worklog?startAt=%d&maxResults=%d", endpoint, issue, startAt, maxResults)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.WorklogWithPagination{}
err := readJSON(resp.Body, results)
if err != nil {
return nil, err
}
startAt = startAt + maxResults
total = results.Total
for _, worklog := range results.Worklogs {
worklogs = append(worklogs, worklog)
}
} else {
return nil, responseError(resp)
}
}
return &worklogs, nil
}
type WorklogProvider interface {
ProvideWorklog() *jiradata.Worklog
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/worklog-addWorklog
func (j *Jira) AddIssueWorklog(issue string, wp WorklogProvider) (*jiradata.Worklog, error) {
return AddIssueWorklog(j.UA, j.Endpoint, issue, wp)
}
func AddIssueWorklog(ua HttpClient, endpoint string, issue string, wp WorklogProvider) (*jiradata.Worklog, error) {
req := wp.ProvideWorklog()
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/worklog", endpoint, issue)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 201 {
results := &jiradata.Worklog{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getEditIssueMeta
func (j *Jira) GetIssueEditMeta(issue string) (*jiradata.EditMeta, error) {
return GetIssueEditMeta(j.UA, j.Endpoint, issue)
}
func GetIssueEditMeta(ua HttpClient, endpoint string, issue string) (*jiradata.EditMeta, error) {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/editmeta", endpoint, issue)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.EditMeta{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
type IssueUpdateProvider interface {
ProvideIssueUpdate() *jiradata.IssueUpdate
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue
func (j *Jira) EditIssue(issue string, iup IssueUpdateProvider) error {
return EditIssue(j.UA, j.Endpoint, issue, iup)
}
func EditIssue(ua HttpClient, endpoint string, issue string, iup IssueUpdateProvider) error {
req := iup.ProvideIssueUpdate()
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", endpoint, issue)
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-createIssue
func (j *Jira) CreateIssue(iup IssueUpdateProvider) (*jiradata.IssueCreateResponse, error) {
return CreateIssue(j.UA, j.Endpoint, iup)
}
func CreateIssue(ua HttpClient, endpoint string, iup IssueUpdateProvider) (*jiradata.IssueCreateResponse, error) {
req := iup.ProvideIssueUpdate()
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s/rest/api/2/issue", endpoint)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 201 {
results := &jiradata.IssueCreateResponse{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getCreateIssueMeta
func (j *Jira) GetIssueCreateMetaProject(projectKey string) (*jiradata.CreateMetaProject, error) {
return GetIssueCreateMetaProject(j.UA, j.Endpoint, projectKey)
}
func GetIssueCreateMetaProject(ua HttpClient, endpoint string, projectKey string) (*jiradata.CreateMetaProject, error) {
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&expand=projects.issuetypes.fields", endpoint, projectKey)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.CreateMeta{}
err = readJSON(resp.Body, results)
if err != nil {
return nil, err
}
for _, project := range results.Projects {
if project.Key == projectKey {
return project, nil
}
}
return nil, fmt.Errorf("Project %s not found", projectKey)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getCreateIssueMeta
func (j *Jira) GetIssueCreateMetaIssueType(projectKey, issueTypeName string) (*jiradata.IssueType, error) {
return GetIssueCreateMetaIssueType(j.UA, j.Endpoint, projectKey, issueTypeName)
}
func GetIssueCreateMetaIssueType(ua HttpClient, endpoint string, projectKey, issueTypeName string) (*jiradata.IssueType, error) {
uri := fmt.Sprintf("%s/rest/api/2/issue/createmeta?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", endpoint, projectKey, issueTypeName)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.CreateMeta{}
err = readJSON(resp.Body, results)
if err != nil {
return nil, err
}
for _, project := range results.Projects {
if project.Key == projectKey {
for _, issueType := range project.IssueTypes {
if issueType.Name == issueTypeName {
return issueType, nil
}
}
}
}
return nil, fmt.Errorf("Project %s and IssueType %s not found", projectKey, issueTypeName)
}
return nil, responseError(resp)
}
type LinkIssueProvider interface {
ProvideLinkIssueRequest() *jiradata.LinkIssueRequest
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issueLink-linkIssues
func (j *Jira) LinkIssues(lip LinkIssueProvider) error {
return LinkIssues(j.UA, j.Endpoint, lip)
}
func LinkIssues(ua HttpClient, endpoint string, lip LinkIssueProvider) error {
req := lip.ProvideLinkIssueRequest()
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issueLink", endpoint)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 201 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-getTransitions
func (j *Jira) GetIssueTransitions(issue string) (*jiradata.TransitionsMeta, error) {
return GetIssueTransitions(j.UA, j.Endpoint, issue)
}
func GetIssueTransitions(ua HttpClient, endpoint string, issue string) (*jiradata.TransitionsMeta, error) {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", endpoint, issue)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := &jiradata.TransitionsMeta{}
return results, readJSON(resp.Body, results)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-doTransition
func (j *Jira) TransitionIssue(issue string, iup IssueUpdateProvider) error {
return TransitionIssue(j.UA, j.Endpoint, issue, iup)
}
func TransitionIssue(ua HttpClient, endpoint string, issue string, iup IssueUpdateProvider) error {
req := iup.ProvideIssueUpdate()
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", endpoint, issue)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issueLinkType-getIssueLinkTypes
func (j *Jira) GetIssueLinkTypes() (*jiradata.IssueLinkTypes, error) {
return GetIssueLinkTypes(j.UA, j.Endpoint)
}
func GetIssueLinkTypes(ua HttpClient, endpoint string) (*jiradata.IssueLinkTypes, error) {
uri := fmt.Sprintf("%s/rest/api/2/issueLinkType", endpoint)
resp, err := ua.GetJSON(uri)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
results := struct {
IssueLinkTypes jiradata.IssueLinkTypes
}{
IssueLinkTypes: jiradata.IssueLinkTypes{},
}
return &results.IssueLinkTypes, readJSON(resp.Body, &results)
}
return nil, responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-addVote
func (j *Jira) IssueAddVote(issue string) error {
return IssueAddVote(j.UA, j.Endpoint, issue)
}
func IssueAddVote(ua HttpClient, endpoint string, issue string) error {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", endpoint, issue)
resp, err := ua.Post(uri, "application/json", strings.NewReader("{}"))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-removeVote
func (j *Jira) IssueRemoveVote(issue string) error {
return IssueRemoveVote(j.UA, j.Endpoint, issue)
}
func IssueRemoveVote(ua HttpClient, endpoint string, issue string) error {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", endpoint, issue)
resp, err := ua.Delete(uri)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
type RankRequestProvider interface {
ProvideRankRequest() *jiradata.RankRequest
}
// https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/issue-rankIssues
func (j *Jira) RankIssues(rrp RankRequestProvider) error {
return RankIssues(j.UA, j.Endpoint, rrp)
}
func RankIssues(ua HttpClient, endpoint string, rrp RankRequestProvider) error {
req := rrp.ProvideRankRequest()
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/agile/1.0/issue/rank", endpoint)
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-addWatcher
func (j *Jira) IssueAddWatcher(issue, user string) error {
return IssueAddWatcher(j.UA, j.Endpoint, issue, user)
}
func IssueAddWatcher(ua HttpClient, endpoint string, issue, user string) error {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", endpoint, issue)
resp, err := ua.Post(uri, "application/json", strings.NewReader(fmt.Sprintf("%q", user)))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-addWatcher
func (j *Jira) IssueRemoveWatcher(issue, user string) error {
return IssueRemoveWatcher(j.UA, j.Endpoint, issue, user)
}
func IssueRemoveWatcher(ua HttpClient, endpoint string, issue, user string) error {
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers?username=%s", endpoint, issue, user)
resp, err := ua.Delete(uri)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
type CommentProvider interface {
ProvideComment() *jiradata.Comment
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/comment-addComment
func (j *Jira) IssueAddComment(issue string, cp CommentProvider) (*jiradata.Comment, error) {
return IssueAddComment(j.UA, j.Endpoint, issue, cp)
}
func IssueAddComment(ua HttpClient, endpoint string, issue string, cp CommentProvider) (*jiradata.Comment, error) {
req := cp.ProvideComment()
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/comment", endpoint, issue)
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 201 {
results := jiradata.Comment{}
return &results, readJSON(resp.Body, &results)
}
return nil, responseError(resp)
}
type UserProvider interface {
ProvideUser() *jiradata.User
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-assign
func (j *Jira) IssueAssign(issue, name string) error {
return IssueAssign(j.UA, j.Endpoint, issue, name)
}
func IssueAssign(ua HttpClient, endpoint string, issue, name string) error {
// this is special, not using the jiradata.User structure
// because we need to be able to send `null` as the name param
// when we want to un-assign the issue
req := struct {
Name *string `json:"name"`
}{&name}
if name == "" {
req.Name = nil
}
encoded, err := json.Marshal(req)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/assignee", endpoint, issue)
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 204 {
return nil
}
return responseError(resp)
}
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/attachments-addAttachment
func (j *Jira) IssueAttachFile(issue, filename string, contents io.Reader) (*jiradata.ListOfAttachment, error) {
return IssueAttachFile(j.UA, j.Endpoint, issue, filename, contents)
}
func IssueAttachFile(ua HttpClient, endpoint string, issue, filename string, contents io.Reader) (*jiradata.ListOfAttachment, error) {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
formFile, err := w.CreateFormFile("file", filename)
if err != nil {
return nil, err
}
_, err = io.Copy(formFile, contents)
if err != nil {
return nil, err
}
uri, err := url.Parse(fmt.Sprintf("%s/rest/api/2/issue/%s/attachments", endpoint, issue))
req := oreo.RequestBuilder(uri).WithMethod("POST").WithHeader(
"X-Atlassian-Token", "no-check",
).WithHeader(
"Accept", "application/json",
).WithContentType(w.FormDataContentType()).WithBody(&buf).Build()
w.Close()
resp, err := ua.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
// FIXME move this to a test, and run go tests as part of our regression
if false {
// this is because schema is wrong, defaults to type `int`, so we manually change it
// to `string`. If the jiradata is regenerated we need to manually make the change
// again.
log.Debugf("Assert Attachment.ID is a string, rather than int: %v", &jiradata.Attachment{
ID: jiradata.IntOrString(0),
})
}
results := jiradata.ListOfAttachment{}
return &results, readJSON(resp.Body, &results)
}
return nil, responseError(resp)
}
+22
View File
@@ -0,0 +1,22 @@
package jira
import (
"github.com/coryb/oreo"
logging "gopkg.in/op/go-logging.v1"
)
var log = logging.MustGetLogger("jira")
const VERSION = "1.0.9"
type Jira struct {
Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
UA HttpClient `json:"-" yaml:"-"`
}
func NewJira(endpoint string) *Jira {
return &Jira{
Endpoint: endpoint,
UA: oreo.New(),
}
}
-444
View File
@@ -1,444 +0,0 @@
package cli
import (
"bytes"
"encoding/json"
"fmt"
"github.com/kballard/go-shellquote"
"github.com/op/go-logging"
"gopkg.in/coryb/yaml.v2"
"io/ioutil"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"os/exec"
"runtime"
"strings"
"time"
)
var log = logging.MustGetLogger("jira.cli")
type Cli struct {
endpoint *url.URL
opts map[string]interface{}
cookieFile string
ua *http.Client
}
func New(opts map[string]interface{}) *Cli {
homedir := os.Getenv("HOME")
cookieJar, _ := cookiejar.New(nil)
endpoint, _ := opts["endpoint"].(string)
url, _ := url.Parse(strings.TrimRight(endpoint, "/"))
if project, ok := opts["project"].(string); ok {
opts["project"] = strings.ToUpper(project)
}
cli := &Cli{
endpoint: url,
opts: opts,
cookieFile: fmt.Sprintf("%s/.jira.d/cookies.js", homedir),
ua: &http.Client{Jar: cookieJar},
}
cli.ua.Jar.SetCookies(url, cli.loadCookies())
return cli
}
func (c *Cli) saveCookies(cookies []*http.Cookie) {
// expiry in one week from now
expiry := time.Now().Add(24 * 7 * time.Hour)
for _, cookie := range cookies {
cookie.Expires = expiry
}
if currentCookies := c.loadCookies(); currentCookies != nil {
currentCookiesByName := make(map[string]*http.Cookie)
for _, cookie := range currentCookies {
currentCookiesByName[cookie.Name] = cookie
}
for _, cookie := range cookies {
currentCookiesByName[cookie.Name] = cookie
}
mergedCookies := make([]*http.Cookie, 0, len(currentCookiesByName))
for _, v := range currentCookiesByName {
mergedCookies = append(mergedCookies, v)
}
jsonWrite(c.cookieFile, mergedCookies)
} else {
jsonWrite(c.cookieFile, cookies)
}
}
func (c *Cli) loadCookies() []*http.Cookie {
bytes, err := ioutil.ReadFile(c.cookieFile)
if err != nil && os.IsNotExist(err) {
// dont load cookies if the file does not exist
return nil
}
if err != nil {
log.Error("Failed to open %s: %s", c.cookieFile, err)
os.Exit(1)
}
cookies := make([]*http.Cookie, 0)
err = json.Unmarshal(bytes, &cookies)
if err != nil {
log.Error("Failed to parse json from file %s: %s", c.cookieFile, err)
}
log.Debug("Loading Cookies: %s", cookies)
return cookies
}
func (c *Cli) post(uri string, content string) (*http.Response, error) {
return c.makeRequestWithContent("POST", uri, content)
}
func (c *Cli) put(uri string, content string) (*http.Response, error) {
return c.makeRequestWithContent("PUT", uri, content)
}
func (c *Cli) makeRequestWithContent(method string, uri string, content string) (*http.Response, error) {
buffer := bytes.NewBufferString(content)
req, _ := http.NewRequest(method, uri, buffer)
log.Info("%s %s", req.Method, req.URL.String())
if log.IsEnabledFor(logging.DEBUG) {
logBuffer := bytes.NewBuffer(make([]byte, 0, len(content)))
req.Write(logBuffer)
log.Debug("%s", logBuffer)
// need to recreate the buffer since the offset is now at the end
// need to be able to rewind the buffer offset, dont know how yet
req, _ = http.NewRequest(method, uri, bytes.NewBufferString(content))
}
if resp, err := c.makeRequest(req); err != nil {
return nil, err
} else {
if resp.StatusCode == 401 {
if err := c.CmdLogin(); err != nil {
return nil, err
}
req, _ = http.NewRequest(method, uri, bytes.NewBufferString(content))
return c.makeRequest(req)
}
return resp, err
}
}
func (c *Cli) get(uri string) (*http.Response, error) {
req, _ := http.NewRequest("GET", uri, nil)
log.Info("%s %s", req.Method, req.URL.String())
if log.IsEnabledFor(logging.DEBUG) {
logBuffer := bytes.NewBuffer(make([]byte, 0))
req.Write(logBuffer)
log.Debug("%s", logBuffer)
}
if resp, err := c.makeRequest(req); err != nil {
return nil, err
} else {
if resp.StatusCode == 401 {
if err := c.CmdLogin(); err != nil {
return nil, err
}
return c.makeRequest(req)
}
return resp, err
}
}
func (c *Cli) makeRequest(req *http.Request) (resp *http.Response, err error) {
req.Header.Set("Content-Type", "application/json")
if resp, err = c.ua.Do(req); err != nil {
log.Error("Failed to %s %s: %s", req.Method, req.URL.String(), err)
return nil, err
} else {
if resp.StatusCode < 200 || resp.StatusCode >= 300 && resp.StatusCode != 401 {
log.Error("response status: %s", resp.Status)
}
runtime.SetFinalizer(resp, func(r *http.Response) {
r.Body.Close()
})
if _, ok := resp.Header["Set-Cookie"]; ok {
c.saveCookies(resp.Cookies())
}
}
return resp, nil
}
func (c *Cli) getTemplate(name string) string {
if override, ok := c.opts["template"].(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 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)
}
}
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"))
if err := mkdir(tmpdir); err != nil {
return err
}
fh, err := ioutil.TempFile(tmpdir, tmpFilePrefix)
if err != nil {
log.Error("Failed to make temp file in %s: %s", tmpdir, err)
return err
}
defer fh.Close()
tmpFileName := fmt.Sprintf("%s.yml", fh.Name())
if err := os.Rename(fh.Name(), tmpFileName); err != nil {
log.Error("Failed to rename %s to %s: %s", fh.Name(), fmt.Sprintf("%s.yml", fh.Name()), err)
return err
}
defer func() {
os.Remove(tmpFileName)
}()
err = runTemplate(template, templateData, fh)
if err != nil {
return err
}
fh.Close()
editor, ok := c.opts["editor"].(string)
if !ok {
editor = os.Getenv("JIRA_EDITOR")
if editor == "" {
editor = os.Getenv("EDITOR")
if editor == "" {
editor = "vim"
}
}
}
editing := c.getOptBool("edit", true)
tmpFileNameOrig := fmt.Sprintf("%s.orig", tmpFileName)
copyFile(tmpFileName, tmpFileNameOrig)
defer func() {
os.Remove(tmpFileNameOrig)
}()
for true {
if editing {
shell, _ := shellquote.Split(editor)
shell = append(shell, tmpFileName)
log.Debug("Running: %#v", shell)
cmd := exec.Command(shell[0], shell[1:]...)
cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin
if err := cmd.Run(); err != nil {
log.Error("Failed to edit template with %s: %s", editor, err)
if promptYN("edit again?", true) {
continue
}
return err
}
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{})
if fh, err := ioutil.ReadFile(tmpFileName); err != nil {
log.Error("Failed to read tmpfile %s: %s", tmpFileName, err)
if editing && promptYN("edit again?", true) {
continue
}
return err
} else {
if err := yaml.Unmarshal(fh, &edited); err != nil {
log.Error("Failed to parse YAML: %s", err)
if editing && promptYN("edit again?", true) {
continue
}
return err
}
}
if fixed, err := yamlFixup(edited); err != nil {
return err
} else {
edited = fixed.(map[string]interface{})
}
// if 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 {
for k := range f {
if _, ok := mf.(map[string]interface{})[k]; !ok {
err := fmt.Errorf("Field %s is not editable", k)
log.Error("%s", err)
if editing && promptYN("edit again?", true) {
continue
}
return err
}
}
}
}
json, err := jsonEncode(edited)
if err != nil {
return err
}
if err := templateProcessor(json); err != nil {
log.Error("%s", err)
if editing && promptYN("edit again?", true) {
continue
}
}
return nil
}
return nil
}
func (c *Cli) Browse(issue string) error {
if val, ok := c.opts["browse"].(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" {
return exec.Command("xdg-open", fmt.Sprintf("%s/browse/%s", c.endpoint, issue)).Run()
}
}
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) 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 {
if val, ok := c.opts[optName].(string); ok {
return val
} else {
return dflt
}
}
func (c *Cli) getOptBool(optName string, dflt bool) bool {
if val, ok := c.opts[optName].(bool); ok {
return val
} else {
return dflt
}
}
-629
View File
@@ -1,629 +0,0 @@
package cli
import (
"bytes"
"fmt"
"golang.org/x/crypto/ssh/terminal"
"net/http"
"net/http/httputil"
"os"
"strings"
// "github.com/kr/pretty"
)
func (c *Cli) CmdLogin() error {
uri := fmt.Sprintf("%s/rest/auth/1/session", c.endpoint)
for true {
req, _ := http.NewRequest("GET", uri, nil)
user, _ := c.opts["user"].(string)
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
if reason := resp.Header.Get("X-Authentication-Denied-Reason"); reason != "" {
err := fmt.Errorf("Authenticaion Failed: %s", reason)
log.Error("%s", err)
return err
}
err := fmt.Errorf("Authentication Failed: Unknown Reason")
log.Error("%s", err)
return err
} else if resp.StatusCode == 200 {
// https://confluence.atlassian.com/display/JIRA043/JIRA+REST+API+%28Alpha%29+Tutorial#JIRARESTAPI%28Alpha%29Tutorial-CAPTCHAs
// probably bad password, try again
if reason := resp.Header.Get("X-Seraph-Loginreason"); reason == "AUTHENTICATION_DENIED" {
log.Warning("Authentication Failed: %s", reason)
continue
}
} else {
log.Warning("Login failed")
continue
}
}
return nil
}
return nil
}
func (c *Cli) CmdFields() error {
log.Debug("fields called")
uri := fmt.Sprintf("%s/rest/api/2/field", c.endpoint)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("fields"), data, nil)
}
func (c *Cli) CmdList() error {
log.Debug("list called")
if data, err := c.FindIssues(); err != nil {
return err
} else {
return runTemplate(c.getTemplate("list"), data, nil)
}
}
func (c *Cli) CmdView(issue string) error {
log.Debug("view called")
c.Browse(issue)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("view"), data, nil)
}
func (c *Cli) CmdEdit(issue string) error {
log.Debug("edit called")
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/editmeta", c.endpoint, issue)
editmeta, err := responseToJson(c.get(uri))
if err != nil {
return err
}
uri = fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
var issueData map[string]interface{}
if data, err := responseToJson(c.get(uri)); err != nil {
return err
} else {
issueData = data.(map[string]interface{})
}
issueData["meta"] = editmeta.(map[string]interface{})
issueData["overrides"] = c.opts
return c.editTemplate(
c.getTemplate("edit"),
fmt.Sprintf("%s-edit-", issue),
issueData,
func(json string) error {
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(issueData["key"].(string))
if ! c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issueData["key"], c.endpoint, issueData["key"])
}
return nil
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From PUT")
log.Error("%s:\n%s", err, logBuffer)
return err
}
},
)
}
func (c *Cli) CmdEditMeta(issue string) error {
log.Debug("editMeta called")
c.Browse(issue)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/editmeta", c.endpoint, issue)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("editmeta"), data, nil)
}
func (c *Cli) CmdTransitionMeta(issue string) error {
log.Debug("tranisionMeta called")
c.Browse(issue)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", c.endpoint, issue)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("transmeta"), data, nil)
}
func (c *Cli) CmdIssueTypes() 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))
if err != nil {
return err
}
return runTemplate(c.getTemplate("issuetypes"), data, nil)
}
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))
if err != nil {
return err
}
if val, ok := data.(map[string]interface{})["projects"]; ok {
if len(val.([]interface{})) == 0 {
err = fmt.Errorf("Project '%s' or issuetype '%s' unknown. Unable to createmeta.", project, issuetype)
log.Error("%s", err)
return err
}
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
data = val.([]interface{})[0]
}
}
return runTemplate(c.getTemplate("createmeta"), data, nil)
}
func (c *Cli) CmdTransitions(issue string) error {
log.Debug("Transitions called")
c.Browse(issue)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", c.endpoint, issue)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("transitions"), data, nil)
}
func (c *Cli) CmdCreate() 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)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
issueData := make(map[string]interface{})
issueData["overrides"] = c.opts
issueData["overrides"].(map[string]interface{})["issuetype"] = issuetype
if val, ok := data.(map[string]interface{})["projects"]; ok {
if len(val.([]interface{})) == 0 {
err = fmt.Errorf("Project '%s' or issuetype '%s' unknown. Unable to create issue.", project, issuetype)
log.Error("%s", err)
return err
}
if val, ok = val.([]interface{})[0].(map[string]interface{})["issuetypes"]; ok {
issueData["meta"] = val.([]interface{})[0]
}
}
sanitizedType := strings.ToLower(strings.Replace(issuetype, " ", "", -1))
return c.editTemplate(
c.getTemplate(fmt.Sprintf("create-%s", sanitizedType)),
fmt.Sprintf("create-%s-", sanitizedType),
issueData,
func(json string) error {
log.Debug("JSON: %s", json)
uri := fmt.Sprintf("%s/rest/api/2/issue", c.endpoint)
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 {
// response: {"id":"410836","key":"PROJ-238","self":"https://jira/rest/api/2/issue/410836"}
if json, err := responseToJson(resp, nil); err != nil {
return err
} else {
key := json.(map[string]interface{})["key"].(string)
link := fmt.Sprintf("%s/browse/%s", c.endpoint, key)
c.Browse(key)
c.SaveData(map[string]string{
"issue": key,
"link": link,
})
if ! c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s\n", key, link)
}
}
return nil
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
},
)
return nil
}
func (c *Cli) CmdIssueLinkTypes() error {
log.Debug("Transitions called")
uri := fmt.Sprintf("%s/rest/api/2/issueLinkType", c.endpoint)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
return runTemplate(c.getTemplate("issuelinktypes"), data, nil)
}
func (c *Cli) CmdBlocks(blocker string, issue string) error {
log.Debug("blocks called")
json, err := jsonEncode(map[string]interface{}{
"type": map[string]string{
"name": "Depends", // TODO This is probably not constant across Jira installs
},
"inwardIssue": map[string]string{
"key": issue,
},
"outwardIssue": map[string]string{
"key": blocker,
},
})
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issueLink", c.endpoint)
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)
if ! c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
return nil
}
func (c *Cli) CmdDups(duplicate string, issue string) error {
log.Debug("dups called")
json, err := jsonEncode(map[string]interface{}{
"type": map[string]string{
"name": "Duplicate", // TODO This is probably not constant across Jira installs
},
"inwardIssue": map[string]string{
"key": duplicate,
},
"outwardIssue": map[string]string{
"key": issue,
},
})
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issueLink", c.endpoint)
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)
if ! c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
return nil
}
func (c *Cli) CmdWatch(issue string) error {
watcher := c.getOptString("watcher", c.opts["user"].(string))
log.Debug("watch called")
json, err := jsonEncode(watcher)
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", c.endpoint, issue)
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)
if ! c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
return nil
}
func (c *Cli) CmdTransition(issue string, trans string) error {
log.Debug("transition called")
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions?expand=transitions.fields", c.endpoint, issue)
data, err := responseToJson(c.get(uri))
if err != nil {
return err
}
transitions := data.(map[string]interface{})["transitions"].([]interface{})
var transId, transName string
var transMeta map[string]interface{}
found := make([]string, 0, len(transitions))
for _, transition := range transitions {
name := transition.(map[string]interface{})["name"].(string)
id := transition.(map[string]interface{})["id"].(string)
found = append(found, name)
if strings.Contains(strings.ToLower(name), trans) {
transName = name
transId = id
transMeta = transition.(map[string]interface{})
}
}
if transId == "" {
err := fmt.Errorf("Invalid Transition '%s', Available: %s", trans, strings.Join(found, ", "))
log.Error("%s", err)
return err
}
handlePost := func(json string) error {
log.Debug("POST: %s", json)
// os.Exit(0)
uri = fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", c.endpoint, issue)
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)
if ! c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
return nil
}
uri = fmt.Sprintf("%s/rest/api/2/issue/%s", c.endpoint, issue)
var issueData map[string]interface{}
if data, err := responseToJson(c.get(uri)); err != nil {
return err
} else {
issueData = data.(map[string]interface{})
}
issueData["meta"] = transMeta
issueData["overrides"] = c.opts
issueData["transition"] = map[string]interface{}{
"name": transName,
"id": transId,
}
return c.editTemplate(
c.getTemplate("transition"),
fmt.Sprintf("%s-trans-%s-", issue, trans),
issueData,
handlePost,
)
}
func (c *Cli) CmdComment(issue string) error {
log.Debug("comment called")
handlePost := func(json string) error {
log.Debug("JSON: %s", json)
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/comment", c.endpoint, issue)
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)
if ! c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
return nil
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From POST")
log.Error("%s:\n%s", err, logBuffer)
return err
}
}
if comment, ok := c.opts["comment"]; ok && comment != "" {
json, err := jsonEncode(map[string]interface{}{
"body": comment,
})
if err != nil {
return err
}
return handlePost(json)
} else {
return c.editTemplate(
c.getTemplate("comment"),
fmt.Sprintf("%s-create-", issue),
map[string]interface{}{},
handlePost,
)
}
return nil
}
func (c *Cli) CmdAssign(issue string, user string) error {
log.Debug("assign called")
json, err := jsonEncode(map[string]interface{}{
"name": user,
})
if err != nil {
return err
}
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/assignee", c.endpoint, issue)
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)
if ! c.opts["quiet"].(bool) {
fmt.Printf("OK %s %s/browse/%s\n", issue, c.endpoint, issue)
}
} else {
logBuffer := bytes.NewBuffer(make([]byte, 0))
resp.Write(logBuffer)
err := fmt.Errorf("Unexpected Response From PUT")
log.Error("%s:\n%s", err, logBuffer)
return err
}
return nil
}
func (c *Cli) CmdExportTemplates() error {
dir := c.opts["directory"].(string)
if err := mkdir(dir); err != nil {
return err
}
for name, template := range all_templates {
if wanted, ok := c.opts["template"]; ok && wanted != name {
continue
}
templateFile := fmt.Sprintf("%s/%s", dir, name)
if _, err := os.Stat(templateFile); err == nil {
log.Warning("Skipping %s, already exists", templateFile)
continue
}
if fh, err := os.OpenFile(templateFile, os.O_WRONLY|os.O_CREATE, 0644); err != nil {
log.Error("Failed to open %s for writing: %s", templateFile, err)
return err
} else {
defer fh.Close()
log.Notice("Creating %s", templateFile)
fh.Write([]byte(template))
}
}
return nil
}
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)
}
-147
View File
@@ -1,147 +0,0 @@
package cli
var all_templates = map[string]string{
"debug": default_debug_template,
"fields": default_debug_template,
"editmeta": default_debug_template,
"transmeta": default_debug_template,
"createmeta": default_debug_template,
"issuelinktypes": default_debug_template,
"list": default_list_template,
"table": default_table_template,
"view": default_view_template,
"edit": default_edit_template,
"transitions": default_transitions_template,
"issuetypes": default_issuetypes_template,
"create": default_create_template,
"comment": default_comment_template,
"transition": default_transition_template,
"request": default_debug_template,
}
const default_debug_template = "{{ . | toJson}}\n"
const default_list_template = "{{ range .issues }}{{ .key | append \":\" | printf \"%-12s\"}} {{ .fields.summary }}\n{{ end }}"
const default_table_template = `+{{ "-" | rep 16 }}+{{ "-" | rep 57 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
| {{ "Issue" | printf "%-14s" }} | {{ "Summary" | printf "%-55s" }} | {{ "Priority" | printf "%-12s" }} | {{ "Status" | printf "%-12s" }} | {{ "Age" | printf "%-10s" }} | {{ "Reporter" | printf "%-12s" }} | {{ "Assignee" | printf "%-12s" }} |
+{{ "-" | rep 16 }}+{{ "-" | rep 57 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
{{ range .issues }}| {{ .key | printf "%-14s"}} | {{ .fields.summary | abbrev 55 | printf "%-55s" }} | {{.fields.priority.name | printf "%-12s" }} | {{.fields.status.name | printf "%-12s" }} | {{.fields.created | age | printf "%-10s" }} | {{if .fields.reporter}}{{ .fields.reporter.name | printf "%-12s"}}{{else}}<unassigned>{{end}} | {{if .fields.assignee }}{{.fields.assignee.name | printf "%-12s" }}{{else}}<unassigned>{{end}} |
{{ end }}+{{ "-" | rep 16 }}+{{ "-" | rep 57 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
`
const default_view_template = `issue: {{ .key }}
created: {{ .fields.created }}
status: {{ .fields.status.name }}
summary: {{ .fields.summary }}
project: {{ .fields.project.key }}
components: {{ range .fields.components }}{{ .name }} {{end}}
issuetype: {{ .fields.issuetype.name }}
assignee: {{ if .fields.assignee }}{{ .fields.assignee.name }}{{end}}
reporter: {{ if .fields.reporter }}{{ .fields.reporter.name }}{{end}}
watchers: {{ range .fields.customfield_10110 }}{{ .name }} {{end}}
blockers: {{ range .fields.issuelinks }}{{if .outwardIssue}}{{ .outwardIssue.key }}[{{.outwardIssue.fields.status.name}}]{{end}}{{end}}
depends: {{ range .fields.issuelinks }}{{if .inwardIssue}}{{ .inwardIssue.key }}[{{.inwardIssue.fields.status.name}}]{{end}}{{end}}
priority: {{ .fields.priority.name }}
description: |
{{ or .fields.description "" | indent 2 }}
comments:
{{ range .fields.comment.comments }} - | # {{.author.name}} at {{.created}}
{{ or .body "" | indent 4}}
{{end}}
`
const default_edit_template = `# issue: {{ .key }}
update:
comment:
- add:
body: |~
{{ or .overrides.comment "" | indent 10 }}
fields:
summary: {{ or .overrides.summary .fields.summary }}
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{if .overrides.components }}{{ range (split "," .overrides.components)}}
- name: {{.}}{{end}}{{else}}{{ range .fields.components }}
- name: {{ .name }}{{end}}{{end}}
assignee:
name: {{ if .overrides.assignee }}{{.overrides.assignee}}{{else}}{{if .fields.assignee }}{{ .fields.assignee.name }}{{end}}{{end}}
reporter:
name: {{ if .overrides.reporter }}{{ .overrides.reporter }}{{else if .fields.reporter}}{{ .fields.reporter.name }}{{end}}
# watchers
customfield_10110: {{ range .fields.customfield_10110 }}
- name: {{ .name }}{{end}}{{if .overrides.watcher}}
- name: {{ .overrides.watcher}}{{end}}
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ or .overrides.priority .fields.priority.name }}
description: |~
{{ or .overrides.description (or .fields.description "") | indent 4 }}
# 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_issuetypes_template = `{{ range .projects }}{{ range .issuetypes }}{{color "+bh"}}{{.name | append ":" | printf "%-13s" }}{{color "reset"}} {{.description}}
{{end}}{{end}}`
const default_create_template = `fields:
project:
key: {{ .overrides.project }}
issuetype:
name: {{ .overrides.issuetype }}
summary: {{ or .overrides.summary "" }}
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ or .overrides.priority "unassigned" }}
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}}
- name: {{ . }}{{end}}
description: |~
{{ or .overrides.description "" | indent 4 }}
assignee:
name: {{ or .overrides.assignee "" }}
reporter:
name: {{ or .overrides.reporter .overrides.user }}
# watchers
customfield_10110: {{ range split "," (or .overrides.watchers "")}}
- name: {{.}}{{end}}
- name:
`
const default_comment_template = `body: |~
{{ or .overrides.comment "" | indent 2 }}
`
const default_transition_template = `update:
comment:
- add:
body: |~
{{ or .overrides.comment "" | indent 10 }}
fields:{{if .meta.fields.assignee}}
assignee:
name: {{if .overrides.assignee}}{{.overrides.assignee}}{{else}}{{if .fields.assignee}}{{.fields.assignee.name}}{{end}}{{end}}{{end}}{{if .meta.fields.components}}
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{if .overrides.components }}{{ range (split "," .overrides.components)}}
- name: {{.}}{{end}}{{else}}{{ range .fields.components }}
- name: {{ .name }}{{end}}{{end}}{{end}}{{if .meta.fields.description}}
description: {{or .overrides.description .fields.description }}{{end}}{{if .meta.fields.fixVersions}}{{if .meta.fields.fixVersions.allowedValues}}
fixVersions: # Values: {{ range .meta.fields.fixVersions.allowedValues }}{{.name}}, {{end}}{{if .overrides.fixVersions}}{{ range (split "," .overrides.fixVersions)}}
- name: {{.}}{{end}}{{else}}{{range .fields.fixVersions}}
- name: {{.}}{{end}}{{end}}{{end}}{{end}}{{if .meta.fields.issuetype}}
issuetype: # Values: {{ range .meta.fields.issuetype.allowedValues }}{{.name}}, {{end}}
name: {{if .overrides.issuetype}}{{.overrides.issuetype}}{{else}}{{if .fields.issuetype}}{{.fields.issuetype.name}}{{end}}{{end}}{{end}}{{if .meta.fields.labels}}
labels: {{range .fields.labels}}
- {{.}}{{end}}{{if .overrides.labels}}{{range (split "," .overrides.labels)}}
- {{.}}{{end}}{{end}}{{end}}{{if .meta.fields.priority}}
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ or .overrides.priority "unassigned" }}{{end}}{{if .meta.fields.reporter}}
reporter:
name: {{if .overrides.reporter}}{{.overrides.reporter}}{{else}}{{if .fields.reporter}}{{.fields.reporter.name}}{{end}}{{end}}{{end}}{{if .meta.fields.resolution}}
resolution: # Values: {{ range .meta.fields.resolution.allowedValues }}{{.name}}, {{end}}
name: {{if .overrides.resolution}}{{.overrides.resolution}}{{else if .fields.resolution}}{{.fields.resolution.name}}{{else}}Fixed{{end}}{{end}}{{if .meta.fields.summary}}
summary: {{or .overrides.summary .fields.summary}}{{end}}{{if .meta.fields.versions.allowedValues}}
versions: # Values: {{ range .meta.fields.versions.allowedValues }}{{.name}}, {{end}}{{if .overrides.versions}}{{ range (split "," .overrides.versions)}}
- name: {{.}}{{end}}{{else}}{{range .fields.versions}}
- name: {{.}}{{end}}{{end}}{{end}}
transition:
id: {{ .transition.id }}
name: {{ .transition.name }}
`
-350
View File
@@ -1,350 +0,0 @@
package cli
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"gopkg.in/coryb/yaml.v2"
"github.com/mgutz/ansi"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"text/template"
"time"
)
func FindParentPaths(fileName string) []string {
cwd, _ := os.Getwd()
paths := make([]string, 0)
// special case if homedir is not in current path then check there anyway
homedir := os.Getenv("HOME")
if !strings.HasPrefix(cwd, homedir) {
file := fmt.Sprintf("%s/%s", homedir, fileName)
if _, err := os.Stat(file); err == nil {
paths = append(paths, file)
}
}
var dir string
for _, part := range strings.Split(cwd, string(os.PathSeparator)) {
if dir == "/" {
dir = fmt.Sprintf("/%s", part)
} else {
dir = fmt.Sprintf("%s/%s", dir, part)
}
file := fmt.Sprintf("%s/%s", dir, fileName)
if _, err := os.Stat(file); err == nil {
paths = append(paths, file)
}
}
return paths
}
func FindClosestParentPath(fileName string) (string, error) {
paths := FindParentPaths(fileName)
if len(paths) > 0 {
return paths[len(paths)-1], nil
}
return "", errors.New(fmt.Sprintf("%s not found in parent directory hierarchy", fileName))
}
func readFile(file string) string {
var bytes []byte
var err error
if bytes, err = ioutil.ReadFile(file); err != nil {
log.Error("Failed to read file %s: %s", file, err)
os.Exit(1)
}
return string(bytes)
}
func 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
} else {
delta := time.Now().Sub(t)
if delta.Minutes() < 2 {
return "a minute", nil
} else if dm := delta.Minutes(); dm < 45 {
return fmt.Sprintf("%d minutes", int(dm)), nil
} else if dm := delta.Minutes(); dm < 90 {
return "an hour", nil
} else if dh := delta.Hours(); dh < 24 {
return fmt.Sprintf("%d hours", int(dh)), nil
} else if dh := delta.Hours(); dh < 48 {
return "a day", nil
} else {
return fmt.Sprintf("%d days", int(delta.Hours()/24)), nil
}
}
return "unknown", nil
}
func 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 {
if out == nil {
out = os.Stdout
}
funcs := map[string]interface{}{
"toJson": func(content interface{}) (string, error) {
if bytes, err := json.MarshalIndent(content, "", " "); err != nil {
return "", err
} else {
return string(bytes), nil
}
},
"append": func(more string, content interface{}) (string, error) {
switch value := content.(type) {
case string:
return string(append([]byte(content.(string)), []byte(more)...)), nil
case []byte:
return string(append(content.([]byte), []byte(more)...)), nil
default:
return "", errors.New(fmt.Sprintf("Unknown type: %s", value))
}
},
"indent": func(spaces int, content string) string {
indent := make([]rune, spaces+1, spaces+1)
indent[0] = '\n'
for i := 1; i < spaces+1; i += 1 {
indent[i] = ' '
}
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)
},
"split": func(sep string, content string) []string {
return strings.Split(content, sep)
},
"abbrev": func(max int, content string) string {
if len(content) > max {
var buffer bytes.Buffer
buffer.WriteString(content[:max-3])
buffer.WriteString("...")
return buffer.String()
}
return content
},
"rep": func(count int, content string) string {
var buffer bytes.Buffer
for i := 0; i < count; i += 1 {
buffer.WriteString(content)
}
return buffer.String()
},
"age": func(content string) (string, error) {
return fuzzyAge(content)
},
"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)
return err
} else {
if err := tmpl.Execute(out, data); err != nil {
log.Error("Failed to execute template: %s", err)
return err
}
}
return nil
}
func responseToJson(resp *http.Response, err error) (interface{}, error) {
if err != nil {
return nil, err
}
data := jsonDecode(resp.Body)
if resp.StatusCode == 400 {
if val, ok := data.(map[string]interface{})["errorMessages"]; ok {
for _, errMsg := range val.([]interface{}) {
log.Error("%s", errMsg)
}
}
}
return data, nil
}
func jsonDecode(io io.Reader) interface{} {
content, err := ioutil.ReadAll(io)
var data interface{}
err = json.Unmarshal(content, &data)
if err != nil {
log.Error("JSON Parse Error: %s from %s", err, content)
}
return data
}
func jsonEncode(data interface{}) (string, error) {
buffer := bytes.NewBuffer(make([]byte, 0))
enc := json.NewEncoder(buffer)
err := enc.Encode(data)
if err != nil {
log.Error("Failed to encode data %s: %s", data, err)
return "", err
}
return buffer.String(), nil
}
func jsonWrite(file string, data interface{}) {
fh, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
defer fh.Close()
if err != nil {
log.Error("Failed to open %s: %s", file, err)
os.Exit(1)
}
enc := json.NewEncoder(fh)
enc.Encode(data)
}
func 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 {
prompt = fmt.Sprintf("%s [y/N]: ", prompt)
} else {
prompt = fmt.Sprintf("%s [Y/n]: ", prompt)
}
fmt.Printf("%s", prompt)
text, _ := reader.ReadString('\n')
ans := strings.ToLower(strings.TrimRight(text, "\n"))
if ans == "" {
return yes
}
if strings.HasPrefix(ans, "y") {
return true
}
return false
}
func yamlFixup(data interface{}) (interface{}, error) {
switch d := data.(type) {
case map[interface{}]interface{}:
// need to copy this map into a string map so json can encode it
copy := make(map[string]interface{})
for key, val := range d {
switch k := key.(type) {
case string:
if fixed, err := yamlFixup(val); err != nil {
return nil, err
} else if fixed != nil {
copy[k] = fixed
}
default:
err := fmt.Errorf("YAML: key %s is type '%T', require 'string'", key, k)
log.Error("%s", err)
return nil, err
}
}
return copy, nil
case map[string]interface{}:
copy := make(map[string]interface{})
for k, v := range d {
if fixed, err := yamlFixup(v); err != nil {
return nil, err
} else if fixed != nil {
copy[k] = fixed
}
}
return copy, nil
case []interface{}:
copy := make([]interface{}, 0, len(d))
for _, val := range d {
if fixed, err := yamlFixup(val); err != nil {
return nil, err
} else if fixed != nil {
copy = append(copy, fixed)
}
}
return copy, nil
case string:
if d == "" || d == "\n" {
return nil, nil
}
return d, nil
default:
return d, nil
}
}
func mkdir(dir string) error {
if stat, err := os.Stat(dir); err != nil && !os.IsNotExist(err) {
log.Error("Failed to stat %s: %s", dir, err)
return err
} else if err == nil && !stat.IsDir() {
err := fmt.Errorf("%s exists and is not a directory!", dir)
log.Error("%s", err)
return err
} else {
// dir does not exist, so try to create it
if err := os.MkdirAll(dir, 0755); err != nil {
log.Error("Failed to mkdir -p %s: %s", dir, err)
return err
}
}
return nil
}
-477
View File
@@ -1,477 +0,0 @@
package main
import (
"bytes"
"fmt"
"github.com/Netflix-Skunkworks/go-jira/jira/cli"
"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}"
buildVersion string
)
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 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 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
-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",
"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",
}
defaults := map[string]interface{}{
"user": user,
"queryfields": defaultQueryFields,
"directory": fmt.Sprintf("%s/.jira.d/templates", home),
"sort": defaultSort,
"max_results": defaultMaxResults,
"method": "GET",
}
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", buildVersion))
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,
"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,
"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,
})
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
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 := cli.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 cli.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)
err = c.CmdWatch(args[0])
case "transition":
requireArgs(2)
setEditing(true)
err = c.CmdTransition(args[0], args[1])
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 "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 "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 := cli.FindParentPaths(".jira.d/config.yml")
// prepend
paths = append([]string{"/etc/jira-cli.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)
}
}
}
+355
View File
@@ -0,0 +1,355 @@
package jiracli
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"reflect"
"strings"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"github.com/jinzhu/copier"
shellquote "github.com/kballard/go-shellquote"
"github.com/tidwall/gjson"
"gopkg.in/AlecAivazis/survey.v1"
kingpin "gopkg.in/alecthomas/kingpin.v2"
yaml "gopkg.in/coryb/yaml.v2"
logging "gopkg.in/op/go-logging.v1"
)
var log = logging.MustGetLogger("jira")
type Exit struct {
Code int
}
type GlobalOptions struct {
Endpoint figtree.StringOption `yaml:"endpoint,omitempty" json:"endpoint,omitempty"`
Insecure figtree.BoolOption `yaml:"insecure,omitempty" json:"insecure,omitempty"`
PasswordSource figtree.StringOption `yaml:"password-source,omitempty" json:"password-source,omitempty"`
Quiet figtree.BoolOption `yaml:"quiet,omitempty" json:"quiet,omitempty"`
UnixProxy figtree.StringOption `yaml:"unixproxy,omitempty" json:"unixproxy,omitempty"`
User figtree.StringOption `yaml:"user,omitempty" json:"user,omitempty"`
}
type CommonOptions struct {
Browse figtree.BoolOption `yaml:"browse,omitempty" json:"browse,omitempty"`
Editor figtree.StringOption `yaml:"editor,omitempty" json:"editor,omitempty"`
GJsonQuery figtree.StringOption `yaml:"gjq,omitempty" json:"gjq,omitempty"`
SkipEditing figtree.BoolOption `yaml:"noedit,omitempty" json:"noedit,omitempty"`
Template figtree.StringOption `yaml:"template,omitempty" json:"template,omitempty"`
}
type CommandRegistryEntry struct {
Help string
UsageFunc func(*figtree.FigTree, *kingpin.CmdClause) error
ExecuteFunc func(*oreo.Client, *GlobalOptions) error
}
type CommandRegistry struct {
Command string
Aliases []string
Entry *CommandRegistryEntry
Default bool
}
// either kingpin.Application or kingpin.CmdClause fit this interface
type kingpinAppOrCommand interface {
Command(string, string) *kingpin.CmdClause
GetCommand(string) *kingpin.CmdClause
}
func Register(app *kingpin.Application, o *oreo.Client, fig *figtree.FigTree, reg []CommandRegistry) {
globals := GlobalOptions{
User: figtree.NewStringOption(os.Getenv("USER")),
}
app.Flag("endpoint", "Base URI to use for Jira").Short('e').SetValue(&globals.Endpoint)
app.Flag("insecure", "Disable TLS certificate verification").Short('k').SetValue(&globals.Insecure)
app.Flag("quiet", "Suppress output to console").Short('Q').SetValue(&globals.Quiet)
app.Flag("unixproxy", "Path for a unix-socket proxy").SetValue(&globals.UnixProxy)
app.Flag("user", "Login name used for authentication with Jira service").Short('u').SetValue(&globals.User)
app.PreAction(func(_ *kingpin.ParseContext) error {
if globals.Insecure.Value {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
o = o.WithTransport(transport)
}
if globals.UnixProxy.Value != "" {
o = o.WithTransport(unixProxy(globals.UnixProxy.Value))
}
return nil
})
o = o.WithPostCallback(
func(req *http.Request, resp *http.Response) (*http.Response, error) {
authUser := resp.Header.Get("X-Ausername")
if authUser == "" || authUser == "anonymous" {
// preserve the --quiet value, we need to temporarily disable it so
// the normal login output is surpressed
defer func(quiet bool) {
globals.Quiet.Value = quiet
}(globals.Quiet.Value)
globals.Quiet.Value = true
// we are not logged in, so force login now by running the "login" command
app.Parse([]string{"login"})
// rerun the original request
return o.Do(req)
}
return resp, nil
},
)
for _, command := range reg {
copy := command
commandFields := strings.Fields(copy.Command)
var appOrCmd kingpinAppOrCommand = app
if len(commandFields) > 1 {
for _, name := range commandFields[0 : len(commandFields)-1] {
tmp := appOrCmd.GetCommand(name)
if tmp == nil {
tmp = appOrCmd.Command(name, "")
}
appOrCmd = tmp
}
}
cmd := appOrCmd.Command(commandFields[len(commandFields)-1], copy.Entry.Help)
LoadConfigs(cmd, fig, &globals)
for _, alias := range copy.Aliases {
cmd = cmd.Alias(alias)
}
if copy.Default {
cmd = cmd.Default()
}
if copy.Entry.UsageFunc != nil {
copy.Entry.UsageFunc(fig, cmd)
}
cmd.Action(
func(_ *kingpin.ParseContext) error {
return copy.Entry.ExecuteFunc(o, &globals)
},
)
}
}
func LoadConfigs(cmd *kingpin.CmdClause, fig *figtree.FigTree, opts interface{}) {
cmd.PreAction(func(_ *kingpin.ParseContext) error {
os.Setenv("JIRA_OPERATION", cmd.FullCommand())
// load command specific configs first
if err := fig.LoadAllConfigs(strings.Join(strings.Fields(cmd.FullCommand()), "_")+".yml", opts); err != nil {
return err
}
// then load generic configs if not already populated above
return fig.LoadAllConfigs("config.yml", opts)
})
}
func BrowseUsage(cmd *kingpin.CmdClause, opts *CommonOptions) {
cmd.Flag("browse", "Open issue(s) in browser after operation").Short('b').SetValue(&opts.Browse)
}
func EditorUsage(cmd *kingpin.CmdClause, opts *CommonOptions) {
cmd.Flag("editor", "Editor to use").SetValue(&opts.Editor)
}
func TemplateUsage(cmd *kingpin.CmdClause, opts *CommonOptions) {
cmd.Flag("template", "Template to use for output").Short('t').SetValue(&opts.Template)
}
func GJsonQueryUsage(cmd *kingpin.CmdClause, opts *CommonOptions) {
cmd.Flag("gjq", "GJSON Query to filter output, see https://goo.gl/iaYwJ5").SetValue(&opts.GJsonQuery)
}
func (o *CommonOptions) PrintTemplate(data interface{}) error {
if o.GJsonQuery.Value != "" {
buf := bytes.NewBufferString("")
RunTemplate("json", data, buf)
results := gjson.GetBytes(buf.Bytes(), o.GJsonQuery.Value)
_, err := os.Stdout.Write([]byte(results.String()))
os.Stdout.Write([]byte{'\n'})
return err
}
return RunTemplate(o.Template.Value, data, nil)
}
func (o *CommonOptions) editFile(fileName string) (changes bool, err error) {
var editor string
for _, ed := range []string{o.Editor.Value, os.Getenv("JIRA_EDITOR"), os.Getenv("EDITOR"), "vim"} {
if ed != "" {
editor = ed
break
}
}
if o.SkipEditing.Value {
return false, nil
}
tmpFileNameOrig := fmt.Sprintf("%s.orig", fileName)
if err := copyFile(fileName, tmpFileNameOrig); err != nil {
return false, err
}
defer func() {
os.Remove(tmpFileNameOrig)
}()
shell, _ := shellquote.Split(editor)
shell = append(shell, fileName)
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 {
return false, err
}
// now we just need to diff the files to see if there are any changes
var oldHandle, newHandle *os.File
var oldStat, newStat os.FileInfo
if oldHandle, err = os.Open(tmpFileNameOrig); err == nil {
if newHandle, err = os.Open(fileName); err == nil {
if oldStat, err = oldHandle.Stat(); err == nil {
if newStat, err = newHandle.Stat(); err == nil {
// different sizes, so must have changes
if oldStat.Size() != newStat.Size() {
return true, err
}
oldBuf, newBuf := make([]byte, 1024), make([]byte, 1024)
var oldCount, newCount int
// loop though 1024 bytes at a time comparing the buffers for changes
for err != io.EOF {
oldCount, _ = oldHandle.Read(oldBuf)
newCount, err = newHandle.Read(newBuf)
if oldCount != newCount {
return true, nil
}
if bytes.Compare(oldBuf[:oldCount], newBuf[:newCount]) != 0 {
return true, nil
}
}
return false, nil
}
}
}
}
return false, err
}
func EditLoop(opts *CommonOptions, input interface{}, output interface{}, submit func() error) error {
tmpFile, err := tmpTemplate(opts.Template.Value, input)
if err != nil {
return err
}
confirm := func(msg string) (answer bool) {
survey.AskOne(
&survey.Confirm{Message: msg, Default: true},
&answer,
nil,
)
return
}
// we need to copy the original output so that we can restore
// it on retries in case we try to populate bogus fields that
// are rejected by the jira service.
dup := reflect.New(reflect.ValueOf(output).Elem().Type())
err = copier.Copy(dup.Interface(), output)
if err != nil {
return err
}
for {
if !opts.SkipEditing.Value {
changes, err := opts.editFile(tmpFile)
if err != nil {
log.Error(err.Error())
if confirm("Editor reported an error, edit again?") {
continue
}
panic(Exit{Code: 1})
}
if !changes {
if !confirm("No changes detected, submit anyway?") {
panic(Exit{Code: 1})
}
}
}
// parse template
data, err := ioutil.ReadFile(tmpFile)
if err != nil {
return err
}
defer func(mapType, iface reflect.Type) {
yaml.DefaultMapType = mapType
yaml.IfaceType = iface
}(yaml.DefaultMapType, yaml.IfaceType)
yaml.DefaultMapType = reflect.TypeOf(map[string]interface{}{})
yaml.IfaceType = yaml.DefaultMapType.Elem()
// restore output incase of retry loop
err = copier.Copy(output, dup.Interface())
if err != nil {
return err
}
// HACK HACK HACK we want to trim out all the yaml garbage that is not
// poplulated, like empty arrays, string values with only a newline,
// etc. We need to do this because jira will reject json documents
// with empty arrays, or empty strings typically. So here we process
// the data to a raw interface{} then we fixup the yaml parsed
// interface, then we serialize to a new yaml document ... then is
// parsed as the original document to populate the output struct. Phew.
var raw interface{}
if err := yaml.Unmarshal(data, &raw); err != nil {
log.Error(err.Error())
if confirm("Invalid YAML syntax, edit again?") {
continue
}
panic(Exit{Code: 1})
}
yamlFixup(&raw)
fixedYAML, err := yaml.Marshal(&raw)
if err != nil {
log.Error(err.Error())
if confirm("Invalid YAML syntax, edit again?") {
continue
}
panic(Exit{Code: 1})
}
if err := yaml.Unmarshal(fixedYAML, output); err != nil {
log.Error(err.Error())
if confirm("Invalid YAML syntax, edit again?") {
continue
}
panic(Exit{Code: 1})
}
// submit template
if err := submit(); err != nil {
log.Error(err.Error())
if confirm("Jira reported an error, edit again?") {
continue
}
panic(Exit{Code: 1})
}
break
}
return nil
}
+13
View File
@@ -0,0 +1,13 @@
package jiracli
import "github.com/pkg/errors"
type Error struct {
error
}
func CliError(cause error) error {
return &Error{
errors.WithStack(cause),
}
}
+17
View File
@@ -0,0 +1,17 @@
// +build !windows
package jiracli
import "github.com/tmc/keyring"
func keyringGet(user string) (string, error) {
password, err := keyring.Get("go-jira", user)
if err != nil && err != keyring.ErrNotFound {
return password, err
}
return password, nil
}
func keyringSet(user, passwd string) error {
return keyring.Set("go-jira", user, passwd)
}
+11
View File
@@ -0,0 +1,11 @@
package jiracli
import "fmt"
func keyringGet(user string) (string, error) {
return "", fmt.Errorf("Keyring is not supported for Windows, see: https://github.com/tmc/keyring")
}
func keyringSet(user, passwd string) error {
return fmt.Errorf("Keyring is not supported for Windows, see: https://github.com/tmc/keyring")
}
+95
View File
@@ -0,0 +1,95 @@
package jiracli
import (
"bytes"
"fmt"
"os/exec"
"strings"
"gopkg.in/AlecAivazis/survey.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
)
func (o *GlobalOptions) ProvideAuthParams() *jiradata.AuthParams {
return &jiradata.AuthParams{
Username: o.User.Value,
Password: o.GetPass(),
}
}
func (o *GlobalOptions) GetPass() string {
passwd := ""
if o.PasswordSource.Value != "" {
if o.PasswordSource.Value == "keyring" {
var err error
passwd, err = keyringGet(o.User.Value)
if err != nil {
panic(err)
}
} else if o.PasswordSource.Value == "pass" {
if bin, err := exec.LookPath("pass"); err == nil {
buf := bytes.NewBufferString("")
cmd := exec.Command(bin, fmt.Sprintf("GoJira/%s", o.User))
cmd.Stdout = buf
cmd.Stderr = buf
if err := cmd.Run(); err == nil {
passwd = strings.TrimSpace(buf.String())
}
}
} else {
log.Warningf("Unknown password-source: %s", o.PasswordSource)
}
}
if passwd != "" {
return passwd
}
err := survey.AskOne(
&survey.Password{
Message: fmt.Sprintf("Jira Password [%s]: ", o.User),
},
&passwd,
nil,
)
if err != nil {
log.Errorf("%s", err)
panic(Exit{Code: 1})
}
o.SetPass(passwd)
return passwd
}
func (o *GlobalOptions) SetPass(passwd string) error {
if o.PasswordSource.Value == "keyring" {
// save password in keychain so that it can be used for subsequent http requests
err := keyringSet(o.User.Value, passwd)
if err != nil {
log.Errorf("Failed to set password in keyring: %s", err)
return err
}
} else if o.PasswordSource.Value == "pass" {
if bin, err := exec.LookPath("pass"); err == nil {
log.Debugf("using %s", bin)
passName := fmt.Sprintf("GoJira/%s", o.User)
if passwd != "" {
in := bytes.NewBufferString(fmt.Sprintf("%s\n%s\n", passwd, passwd))
out := bytes.NewBufferString("")
cmd := exec.Command(bin, "insert", "--force", passName)
cmd.Stdin = in
cmd.Stdout = out
cmd.Stderr = out
if err := cmd.Run(); err != nil {
return fmt.Errorf("Failed to insert password: %s", out.String())
}
} else {
// clear the `pass` entry on empty password
if err := exec.Command(bin, "rm", "--force", passName).Run(); err != nil {
return fmt.Errorf("Failed to clear password for %s", passName)
}
}
}
} else if o.PasswordSource.Value != "" {
return fmt.Errorf("Unknown password-source: %s", o.PasswordSource)
}
return nil
}
+534
View File
@@ -0,0 +1,534 @@
package jiracli
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"text/template"
yaml "gopkg.in/coryb/yaml.v2"
"github.com/coryb/figtree"
"github.com/mgutz/ansi"
"golang.org/x/crypto/ssh/terminal"
)
func findTemplate(name string) ([]byte, error) {
if file, err := findClosestParentPath(filepath.Join(".jira.d", "templates", name)); err == nil {
b, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
return b, nil
}
return nil, nil
}
func getTemplate(name string) (string, error) {
if _, err := os.Stat(name); err == nil {
b, err := ioutil.ReadFile(name)
if err != nil {
return "", err
}
return string(b), nil
}
b, err := findTemplate(name)
if err != nil {
return "", err
} else if b != nil {
return string(b), nil
}
if s, ok := AllTemplates[name]; ok {
return s, nil
}
return "", fmt.Errorf("No Template found for %q", name)
}
func tmpTemplate(templateName string, data interface{}) (string, error) {
tmpFile, err := tmpYml(templateName)
if err != nil {
return "", err
}
defer tmpFile.Close()
return tmpFile.Name(), RunTemplate(templateName, data, tmpFile)
}
func TemplateProcessor() *template.Template {
funcs := map[string]interface{}{
"jira": func() string {
return os.Args[0]
},
"toJson": func(content interface{}) (string, error) {
bytes, err := json.MarshalIndent(content, "", " ")
if err != nil {
return "", err
}
return string(bytes), nil
},
"termWidth": func() int {
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
if err == nil {
return w
}
if os.Getenv("COLUMNS") != "" {
w, err = strconv.Atoi(os.Getenv("COLUMNS"))
}
if err == nil {
return w
}
return 120
},
"sub": func(a, b int) int {
return a - b
},
"append": func(more string, content interface{}) (string, error) {
switch value := content.(type) {
case string:
return string(append([]byte(content.(string)), []byte(more)...)), nil
case []byte:
return string(append(content.([]byte), []byte(more)...)), nil
default:
return "", fmt.Errorf("Unknown type: %s", value)
}
},
"indent": func(spaces int, content string) string {
indent := make([]rune, spaces+1, spaces+1)
indent[0] = '\n'
for i := 1; i < spaces+1; i++ {
indent[i] = ' '
}
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)
},
"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
buffer.WriteString(content[:max-3])
buffer.WriteString("...")
return buffer.String()
}
return content
},
"rep": func(count int, content string) string {
var buffer bytes.Buffer
for i := 0; i < count; i++ {
buffer.WriteString(content)
}
return buffer.String()
},
"age": func(content string) (string, error) {
return fuzzyAge(content)
},
"dateFormat": func(format string, content string) (string, error) {
return dateFormat(format, content)
},
}
return template.New("gojira").Funcs(funcs)
}
func ConfigTemplate(fig *figtree.FigTree, template, command string, opts interface{}) (string, error) {
var tmp interface{}
err := ConvertType(opts, &tmp)
if err != nil {
return "", err
}
fig.LoadAllConfigs(command+".yml", tmp)
fig.LoadAllConfigs("config.yml", tmp)
tmpl, err := TemplateProcessor().Parse(template)
if err != nil {
return "", err
}
buf := bytes.NewBufferString("")
if err := tmpl.Execute(buf, &tmp); err != nil {
return "", err
}
return buf.String(), nil
}
func ConvertType(input interface{}, output interface{}) error {
// HACK HACK HACK: convert data formats to json for backwards compatibilty with templates
jsonData, err := json.Marshal(input)
if err != nil {
return err
}
defer func(mapType, iface reflect.Type) {
yaml.DefaultMapType = mapType
yaml.IfaceType = iface
}(yaml.DefaultMapType, yaml.IfaceType)
yaml.DefaultMapType = reflect.TypeOf(map[string]interface{}{})
yaml.IfaceType = yaml.DefaultMapType.Elem()
if err := yaml.Unmarshal(jsonData, output); err != nil {
return err
}
return nil
}
func RunTemplate(templateName string, data interface{}, out io.Writer) error {
templateContent, err := getTemplate(templateName)
if err != nil {
return err
}
if out == nil {
out = os.Stdout
}
var rawData interface{}
err = ConvertType(data, &rawData)
if err != nil {
return err
}
tmpl, err := TemplateProcessor().Parse(templateContent)
if err != nil {
return err
}
if err := tmpl.Execute(out, rawData); err != nil {
return err
}
return nil
}
var AllTemplates = map[string]string{
"attach-list": defaultAttachListTemplate,
"comment": defaultCommentTemplate,
"component-add": defaultComponentAddTemplate,
"components": defaultComponentsTemplate,
"create": defaultCreateTemplate,
"createmeta": defaultDebugTemplate,
"debug": defaultDebugTemplate,
"edit": defaultEditTemplate,
"editmeta": defaultDebugTemplate,
"epic-create": defaultEpicCreateTemplate,
"epic-list": defaultTableTemplate,
"fields": defaultDebugTemplate,
"issuelinktypes": defaultDebugTemplate,
"issuetypes": defaultIssuetypesTemplate,
"json": defaultDebugTemplate,
"list": defaultListTemplate,
"request": defaultDebugTemplate,
"subtask": defaultSubtaskTemplate,
"table": defaultTableTemplate,
"transition": defaultTransitionTemplate,
"transitions": defaultTransitionsTemplate,
"transmeta": defaultDebugTemplate,
"view": defaultViewTemplate,
"worklog": defaultWorklogTemplate,
"worklogs": defaultWorklogsTemplate,
}
const defaultDebugTemplate = "{{ . | toJson}}\n"
const defaultListTemplate = "{{ range .issues }}{{ .key | append \":\" | printf \"%-12s\"}} {{ .fields.summary }}\n{{ end }}"
const defaultTableTemplate = `{{/* table template */ -}}
{{$w := sub termWidth 107 -}}
+{{ "-" | rep 16 }}+{{ "-" | rep $w }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
| {{ "Issue" | printf "%-14s" }} | {{ "Summary" | printf (printf "%%-%ds" (sub $w 2)) }} | {{ "Type" | printf "%-12s"}} | {{ "Priority" | printf "%-12s" }} | {{ "Status" | printf "%-12s" }} | {{ "Age" | printf "%-10s" }} | {{ "Reporter" | printf "%-12s" }} | {{ "Assignee" | printf "%-12s" }} |
+{{ "-" | rep 16 }}+{{ "-" | rep $w }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
{{ range .issues -}}
| {{ .key | printf "%-14s"}} | {{ .fields.summary | abbrev (sub $w 2) | printf (printf "%%-%ds" (sub $w 2)) }} | {{.fields.issuetype.name | printf "%-12s" }} | {{.fields.priority.name | printf "%-12s" }} | {{.fields.status.name | printf "%-12s" }} | {{.fields.created | age | printf "%-10s" }} | {{if .fields.reporter}}{{ .fields.reporter.name | printf "%-12s"}}{{else}}<unassigned>{{end}} | {{if .fields.assignee }}{{.fields.assignee.name | printf "%-12s" }}{{else}}<unassigned>{{end}} |
{{ end -}}
+{{ "-" | rep 16 }}+{{ "-" | rep $w }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
`
const defaultAttachListTemplate = `{{/* table template */ -}}
+{{ "-" | rep 12 }}+{{ "-" | rep 30 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
| {{printf "%-10s" "id"}} | {{printf "%-28s" "filename"}} | {{printf "%-10s" "bytes"}} | {{printf "%-12s" "user"}} | {{printf "%-12s" "created"}} |
+{{ "-" | rep 12 }}+{{ "-" | rep 30 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
{{range . -}}
| {{.id | printf "%10d" }} | {{.filename | printf "%-28s"}} | {{.size | printf "%10d"}} | {{.author.name | printf "%-12s"}} | {{.created | age | printf "%-12s"}} |
{{end -}}
+{{ "-" | rep 12 }}+{{ "-" | rep 30 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+
`
const defaultViewTemplate = `{{/* view template */ -}}
issue: {{ .key }}
{{if .fields.created -}}
created: {{ .fields.created | age }} ago
{{end -}}
{{if .fields.status -}}
status: {{ .fields.status.name }}
{{end -}}
summary: {{ .fields.summary }}
project: {{ .fields.project.key }}
{{if .fields.components -}}
components: {{ range .fields.components }}{{ .name }} {{end}}
{{end -}}
{{if .fields.issuetype -}}
issuetype: {{ .fields.issuetype.name }}
{{end -}}
{{if .fields.assignee -}}
assignee: {{ .fields.assignee.name }}
{{end -}}
reporter: {{ if .fields.reporter }}{{ .fields.reporter.name }}{{end}}
{{if .fields.customfield_10110 -}}
watchers: {{ range .fields.customfield_10110 }}{{ .name }} {{end}}
{{end -}}
{{if .fields.issuelinks -}}
blockers: {{ range .fields.issuelinks }}{{if .outwardIssue}}{{ .outwardIssue.key }}[{{.outwardIssue.fields.status.name}}]{{end}}{{end}}
depends: {{ range .fields.issuelinks }}{{if .inwardIssue}}{{ .inwardIssue.key }}[{{.inwardIssue.fields.status.name}}]{{end}}{{end}}
{{end -}}
{{if .fields.priority -}}
priority: {{ .fields.priority.name }}
{{end -}}
{{if .fields.votes -}}
votes: {{ .fields.votes.votes}}
{{end -}}
{{if .fields.labels -}}
labels: {{ join ", " .fields.labels }}
{{end -}}
description: |
{{ or .fields.description "" | indent 2 }}
{{if .fields.comment.comments}}
comments:
{{ range .fields.comment.comments }} - | # {{.author.name}}, {{.created | age}} ago
{{ or .body "" | indent 4}}
{{end}}
{{end -}}
`
const defaultEditTemplate = `{{/* edit template */ -}}
# issue: {{ .key }}
update:
comment:
- add:
body: |~
{{ or .overrides.comment "" | indent 10 }}
fields:
summary: >-
{{ or .overrides.summary .fields.summary }}
{{- if and .meta.fields.components .meta.fields.components.allowedValues }}
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{if .overrides.components }}{{ range (split "," .overrides.components)}}
- name: {{.}}{{end}}{{else}}{{ range .fields.components }}
- name: {{ .name }}{{end}}{{end}}{{end}}
{{- if .meta.fields.assignee}}
assignee:
name: {{ if .overrides.assignee }}{{.overrides.assignee}}{{else}}{{if .fields.assignee }}{{ .fields.assignee.name }}{{end}}{{end}}{{end}}
{{- if .meta.fields.reporter}}
reporter:
name: {{ if .overrides.reporter }}{{ .overrides.reporter }}{{else if .fields.reporter}}{{ .fields.reporter.name }}{{end}}{{end}}
{{- if .meta.fields.customfield_10110}}
# watchers
customfield_10110: {{ range .fields.customfield_10110 }}
- name: {{ .name }}{{end}}{{if .overrides.watcher}}
- name: {{ .overrides.watcher}}{{end}}{{end}}
{{- if .meta.fields.priority }}
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ or .overrides.priority .fields.priority.name }}{{end}}
description: |~
{{ or .overrides.description (or .fields.description "") | indent 4 }}
# comments:
# {{ range .fields.comment.comments }} - | # {{.author.name}}, {{.created | age}} ago
# {{ or .body "" | indent 4 | comment}}
# {{end}}
`
const defaultTransitionsTemplate = `{{ range .transitions }}{{.id }}: {{.name}}
{{end}}`
const defaultComponentsTemplate = `{{ range . }}{{.id }}: {{.name}}
{{end}}`
const defaultComponentAddTemplate = `{{/* compoinent add template */ -}}
project: {{or .project ""}}
name: {{or .name ""}}
description: {{or .description ""}}
leadUserName: {{or .leadUserName ""}}
`
const defaultIssuetypesTemplate = `{{/* issuetypes template */ -}}
{{ range .issuetypes }}{{color "+bh"}}{{.name | append ":" | printf "%-13s" }}{{color "reset"}} {{.description}}
{{end}}`
const defaultCreateTemplate = `{{/* create template */ -}}
fields:
project:
key: {{ or .overrides.project "" }}
issuetype:
name: {{ or .overrides.issuetype "" }}
summary: >-
{{ or .overrides.summary "" }}{{if .meta.fields.priority.allowedValues}}
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ or .overrides.priority ""}}{{end}}{{if .meta.fields.components.allowedValues}}
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}}
- name: {{ . }}{{end}}{{end}}
description: |~
{{ or .overrides.description "" | indent 4 }}{{if .meta.fields.assignee}}
assignee:
name: {{ or .overrides.assignee "" }}{{end}}{{if .meta.fields.reporter}}
reporter:
name: {{ or .overrides.reporter .overrides.user }}{{end}}{{if .meta.fields.customfield_10110}}
# watchers
customfield_10110: {{ range split "," (or .overrides.watchers "")}}
- name: {{.}}{{end}}
- name:{{end}}`
const defaultEpicCreateTemplate = `{{/* epic create template */ -}}
fields:
project:
key: {{ or .overrides.project "" }}
# Epic Name
customfield_10120: {{ or (index .overrides "epic-name") "" }}
summary: >-
{{ or .overrides.summary "" }}{{if .meta.fields.priority.allowedValues}}
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ or .overrides.priority ""}}{{end}}{{if .meta.fields.components.allowedValues}}
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}}
- name: {{ . }}{{end}}{{end}}
description: |~
{{ or .overrides.description "" | indent 4 }}{{if .meta.fields.assignee}}
assignee:
name: {{ or .overrides.assignee "" }}{{end}}{{if .meta.fields.reporter}}
reporter:
name: {{ or .overrides.reporter .overrides.user }}{{end}}{{if .meta.fields.customfield_10110}}
# watchers
customfield_10110: {{ range split "," (or .overrides.watchers "")}}
- name: {{.}}{{end}}
- name:{{end}}
issuetype:
name: Epic`
const defaultSubtaskTemplate = `{{/* create subtask template */ -}}
fields:
project:
key: {{ .parent.fields.project.key }}
summary: >-
{{ or .overrides.summary "" }}{{if .meta.fields.priority.allowedValues}}
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ or .overrides.priority ""}}{{end}}{{if .meta.fields.components.allowedValues}}
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}}
- name: {{ . }}{{end}}{{end}}
description: |~
{{ or .overrides.description "" | indent 4 }}{{if .meta.fields.assignee}}
assignee:
name: {{ or .overrides.assignee "" }}{{end}}{{if .meta.fields.reporter}}
reporter:
name: {{ or .overrides.reporter .overrides.user }}{{end}}{{if .meta.fields.customfield_10110}}
# watchers
customfield_10110: {{ range split "," (or .overrides.watchers "")}}
- name: {{.}}{{end}}
- name:{{end}}
issuetype:
name: Sub-task
parent:
key: {{ .parent.key }}`
const defaultCommentTemplate = `body: |~
{{ or .overrides.comment "" | indent 2 }}
`
const defaultTransitionTemplate = `{{/* transition template */ -}}
{{- if .meta.fields.comment }}
update:
comment:
- add:
body: |~
{{ or .overrides.comment "" | indent 10 }}
{{- end -}}
fields:
{{- if .meta.fields.assignee}}
assignee:
name: {{if .overrides.assignee}}{{.overrides.assignee}}{{else}}{{if .fields.assignee}}{{.fields.assignee.name}}{{end}}{{end}}
{{- end -}}
{{if .meta.fields.components}}
components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{if .overrides.components }}{{ range (split "," .overrides.components)}}
- name: {{.}}{{end}}{{else}}{{ range .fields.components }}
- name: {{ .name }}{{end}}{{end}}
{{- end -}}
{{if .meta.fields.description}}
description: {{or .overrides.description .fields.description }}
{{- end -}}
{{if .meta.fields.fixVersions -}}
{{if .meta.fields.fixVersions.allowedValues}}
fixVersions: # Values: {{ range .meta.fields.fixVersions.allowedValues }}{{.name}}, {{end}}{{if .overrides.fixVersions}}{{ range (split "," .overrides.fixVersions)}}
- name: {{.name}}{{end}}{{else}}{{range .fields.fixVersions}}
- name: {{.name}}{{end}}{{end}}
{{- end -}}
{{- end -}}
{{if .meta.fields.issuetype}}
issuetype: # Values: {{ range .meta.fields.issuetype.allowedValues }}{{.name}}, {{end}}
name: {{if .overrides.issuetype}}{{.overrides.issuetype}}{{else}}{{if .fields.issuetype}}{{.fields.issuetype.name}}{{end}}{{end}}
{{- end -}}
{{if .meta.fields.labels}}
labels: {{range .fields.labels}}
- {{.}}{{end}}{{if .overrides.labels}}{{range (split "," .overrides.labels)}}
- {{.}}{{end}}{{end}}
{{- end -}}
{{if .meta.fields.priority}}
priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}}
name: {{ or .overrides.priority "unassigned" }}
{{- end -}}
{{if .meta.fields.reporter}}
reporter:
name: {{if .overrides.reporter}}{{.overrides.reporter}}{{else}}{{if .fields.reporter}}{{.fields.reporter.name}}{{end}}{{end}}
{{- end -}}
{{if .meta.fields.resolution}}
resolution: # Values: {{ range .meta.fields.resolution.allowedValues }}{{.name}}, {{end}}
name: {{if .overrides.resolution}}{{.overrides.resolution}}{{else if .fields.resolution}}{{.fields.resolution.name}}{{else}}{{or .overrides.defaultResolution "Fixed"}}{{end}}
{{- end -}}
{{if .meta.fields.summary}}
summary: >-
{{or .overrides.summary .fields.summary}}
{{- end -}}
{{if .meta.fields.versions.allowedValues}}
versions: # Values: {{ range .meta.fields.versions.allowedValues }}{{.name}}, {{end}}{{if .overrides.versions}}{{ range (split "," .overrides.versions)}}
- name: {{.}}{{end}}{{else}}{{range .fields.versions}}
- name: {{.}}{{end}}{{end}}
{{- end}}
transition:
id: {{ .transition.id }}
name: {{ .transition.name }}
`
const defaultWorklogTemplate = `{{/* worklog template */ -}}
# issue: {{ .issue }}
comment: |~
{{ or .comment "" }}
timeSpent: {{ or .timeSpent "" }}
started: {{ or .started "" }}
`
const defaultWorklogsTemplate = `{{/* worklogs template */ -}}
{{ range .worklogs }}- # {{.author.name}}, {{.created | age}} ago
comment: {{ or .comment "" }}
started: {{ .started }}
timeSpent: {{ .timeSpent }}
{{end}}`
+41
View File
@@ -0,0 +1,41 @@
package jiracli
import (
"fmt"
"net"
"net/http"
"os"
"time"
)
type transport struct {
shadow *http.Transport
}
func newUnixProxyTransport(path string) *transport {
dial := func(network, addr string) (net.Conn, error) {
return net.Dial("unix", path)
}
shadow := &http.Transport{
Dial: dial,
DialTLS: dial,
DisableKeepAlives: true,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: 10 * time.Second,
}
return &transport{shadow}
}
func unixProxy(path string) *transport {
return newUnixProxyTransport(os.ExpandEnv(path))
}
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
req2 := *req
url2 := *req.URL
req2.URL = &url2
req2.URL.Opaque = fmt.Sprintf("//%s%s", req.URL.Host, req.URL.EscapedPath())
return t.shadow.RoundTrip(&req2)
}
+176
View File
@@ -0,0 +1,176 @@
package jiracli
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"runtime"
"time"
kingpin "gopkg.in/alecthomas/kingpin.v2"
"github.com/coryb/figtree"
)
func Homedir() string {
if runtime.GOOS == "windows" {
return os.Getenv("USERPROFILE")
}
return os.Getenv("HOME")
}
func findClosestParentPath(fileName string) (string, error) {
paths := figtree.FindParentPaths(fileName)
if len(paths) > 0 {
return paths[len(paths)-1], nil
}
return "", errors.New(fmt.Sprintf("%s not found in parent directory hierarchy", fileName))
}
func tmpYml(tmpFilePrefix string) (*os.File, error) {
fh, err := ioutil.TempFile("", tmpFilePrefix)
if err != nil {
return nil, err
}
// now we need to rename the file since we dont control the file extensions
// ... it has to be `.yml` so that vim/emacs etc know what edit mode to apply
// for easier editing
oldFileName := fh.Name()
newFileName := oldFileName + ".yml"
// close tmpfile so we can rename on windows
fh.Close()
if err := os.Rename(oldFileName, newFileName); err != nil {
return nil, err
}
return os.OpenFile(newFileName, os.O_RDWR|os.O_EXCL, 0600)
}
func FlagValue(ctx *kingpin.ParseContext, name string) string {
for _, elem := range ctx.Elements {
if flag, ok := elem.Clause.(*kingpin.FlagClause); ok {
if flag.Model().Name == name {
return *elem.Value
}
}
}
return ""
}
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) {
t, err := time.Parse("2006-01-02T15:04:05.000-0700", start)
if err != nil {
return "", err
}
delta := time.Now().Sub(t)
if delta.Minutes() < 2 {
return "a minute", nil
} else if dm := delta.Minutes(); dm < 45 {
return fmt.Sprintf("%d minutes", int(dm)), nil
} else if dm := delta.Minutes(); dm < 90 {
return "an hour", nil
} else if dh := delta.Hours(); dh < 24 {
return fmt.Sprintf("%d hours", int(dh)), nil
} else if dh := delta.Hours(); dh < 48 {
return "a day", nil
}
return fmt.Sprintf("%d days", int(delta.Hours()/24)), nil
}
func dateFormat(format string, content string) (string, error) {
t, err := time.Parse("2006-01-02T15:04:05.000-0700", content)
if err != nil {
return "", err
}
return t.Format(format), nil
}
// this is a HACK to make yaml parsed documents to be serializable
// to json, so prevent this:
// json: unsupported type: map[interface {}]interface {}
// Also we want to clean up common input errors for the edit
// templates, like dangling "\n"
func yamlFixup(data interface{}) (interface{}, error) {
switch d := data.(type) {
case map[interface{}]interface{}:
// need to copy this map into a string map so json can encode it
copy := make(map[string]interface{})
for key, val := range d {
switch k := key.(type) {
case string:
if fixed, err := yamlFixup(val); err != nil {
return nil, err
} else if fixed != nil {
copy[k] = fixed
}
default:
err := fmt.Errorf("YAML: key %s is type '%T', require 'string'", key, k)
log.Errorf("%s", err)
return nil, err
}
}
if len(copy) == 0 {
return nil, nil
}
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 {
copy[k] = fixed
}
}
if len(copy) == 0 {
return nil, nil
}
return copy, nil
case []interface{}:
copy := make([]interface{}, 0, len(d))
for _, val := range d {
if fixed, err := yamlFixup(val); err != nil {
return nil, err
} else if fixed != nil {
copy = append(copy, fixed)
}
}
if len(copy) == 0 {
return nil, nil
}
return copy, nil
case *interface{}:
if fixed, err := yamlFixup(*d); err != nil {
return nil, err
} else if fixed != nil {
*d = fixed
}
return d, nil
case string:
if d == "" || d == "\n" {
return nil, nil
}
return d, nil
default:
return d, nil
}
}
+63
View File
@@ -0,0 +1,63 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type AssignOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
Assignee string `yaml:"assignee,omitempty" json:"assignee,omitempty"`
}
func CmdAssignRegistry() *jiracli.CommandRegistryEntry {
opts := AssignOptions{}
return &jiracli.CommandRegistryEntry{
"Assign user to issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdAssignUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdAssign(o, globals, &opts)
},
}
}
func CmdAssignUsage(cmd *kingpin.CmdClause, opts *AssignOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Flag("default", "use default user for assignee").PreAction(func(ctx *kingpin.ParseContext) error {
if jiracli.FlagValue(ctx, "default") == "true" {
opts.Assignee = "-1"
}
return nil
}).Bool()
cmd.Arg("ISSUE", "issue to assign").Required().StringVar(&opts.Issue)
cmd.Arg("ASSIGNEE", "user to assign to issue").StringVar(&opts.Assignee)
return nil
}
// CmdAssign will assign an issue to a user
func CmdAssign(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AssignOptions) error {
err := jira.IssueAssign(o, globals.Endpoint.Value, opts.Issue, opts.Assignee)
if err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+99
View File
@@ -0,0 +1,99 @@
package jiracmd
import (
"fmt"
"os"
"sort"
"golang.org/x/crypto/ssh/terminal"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
jira "gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
yaml "gopkg.in/coryb/yaml.v2"
)
type AttachCreateOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
Attachment string `yaml:"attachment,omitempty" json:"attachment,omitempty"`
Filename string `yaml:"filename,omitempty" json:"filename,omitempty"`
SaveFile string `yaml:"savefile,omitempty" json:"savefile,omitempty"`
}
func CmdAttachCreateRegistry() *jiracli.CommandRegistryEntry {
opts := AttachCreateOptions{}
return &jiracli.CommandRegistryEntry{
"Attach file to issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdAttachCreateUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdAttachCreate(o, globals, &opts)
},
}
}
func CmdAttachCreateUsage(cmd *kingpin.CmdClause, opts *AttachCreateOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Flag("saveFile", "Write attachment information as yaml to file").StringVar(&opts.SaveFile)
cmd.Flag("filename", "Filename to use for attachment").Short('f').StringVar(&opts.Filename)
cmd.Arg("ISSUE", "issue to assign").Required().StringVar(&opts.Issue)
cmd.Arg("ATTACHMENT", "File to attach to issue, if not provided read from stdin").StringVar(&opts.Attachment)
return nil
}
func CmdAttachCreate(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AttachCreateOptions) error {
var contents *os.File
var err error
if opts.Attachment == "" {
if terminal.IsTerminal(int(os.Stdin.Fd())) {
return fmt.Errorf("ATTACHMENT argument required or redirect from STDIN")
}
contents = os.Stdin
if opts.Filename == "" {
return fmt.Errorf("--filename required when reading from stdin")
}
} else {
contents, err = os.Open(opts.Attachment)
if err != nil {
return err
}
if opts.Filename == "" {
opts.Filename = opts.Attachment
}
}
attachments, err := jira.IssueAttachFile(o, globals.Endpoint.Value, opts.Issue, opts.Filename, contents)
if err != nil {
return err
}
sort.Sort(sort.Reverse(attachments))
if opts.SaveFile != "" {
fh, err := os.Create(opts.SaveFile)
if err != nil {
return err
}
defer fh.Close()
out, err := yaml.Marshal((*attachments)[0])
if err != nil {
return err
}
fh.Write(out)
}
if !globals.Quiet.Value {
fmt.Printf("OK %d %s\n", (*attachments)[0].ID, (*attachments)[0].Content)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+79
View File
@@ -0,0 +1,79 @@
package jiracmd
import (
"fmt"
"io"
"os"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
jira "gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type AttachGetOptions struct {
AttachmentID string `yaml:"attachment-id,omitempty" json:"attachment-id,omitempty"`
OutputFile string `yaml:"output-file,omitempty" json:"output-file,omitempty"`
}
func CmdAttachGetRegistry() *jiracli.CommandRegistryEntry {
opts := AttachGetOptions{}
return &jiracli.CommandRegistryEntry{
"Fetch attachment",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdAttachGetUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdAttachGet(o, globals, &opts)
},
}
}
func CmdAttachGetUsage(cmd *kingpin.CmdClause, opts *AttachGetOptions) error {
cmd.Flag("output", "Write attachment to specified file name, '-' for stdout").Short('o').StringVar(&opts.OutputFile)
cmd.Arg("ATTACHMENT-ID", "Attachment id to fetch").StringVar(&opts.AttachmentID)
return nil
}
func CmdAttachGet(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AttachGetOptions) error {
attachment, err := jira.GetAttachment(o, globals.Endpoint.Value, opts.AttachmentID)
if err != nil {
return err
}
resp, err := o.Get(attachment.Content)
if err != nil {
return err
}
defer resp.Body.Close()
var output *os.File
if opts.OutputFile == "-" {
output = os.Stdout
} else if opts.OutputFile != "" {
output, err = os.Create(opts.OutputFile)
if err != nil {
return err
}
defer output.Close()
} else {
output, err = os.Create(attachment.Filename)
if err != nil {
return err
}
defer output.Close()
}
_, err = io.Copy(output, resp.Body)
if err != nil {
return err
}
output.Close()
if opts.OutputFile != "-" && !globals.Quiet.Value {
fmt.Printf("OK Wrote %s\n", output.Name())
}
return nil
}
+67
View File
@@ -0,0 +1,67 @@
package jiracmd
import (
"sort"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type AttachListOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
}
func CmdAttachListRegistry() *jiracli.CommandRegistryEntry {
opts := AttachListOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("attach-list"),
},
}
return &jiracli.CommandRegistryEntry{
"Prints issue details",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdAttachListUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdAttachList(o, globals, &opts)
},
}
}
func CmdAttachListUsage(cmd *kingpin.CmdClause, opts *AttachListOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Arg("ISSUE", "Issue id to lookup attachments").Required().StringVar(&opts.Issue)
return nil
}
func CmdAttachList(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AttachListOptions) error {
data, err := jira.GetIssue(o, globals.Endpoint.Value, opts.Issue, nil)
if err != nil {
return err
}
// need to conver the interface{} "attachment" field to an actual
// ListOfAttachment object so we can sort it
var attachments jiradata.ListOfAttachment
err = jiracli.ConvertType(data.Fields["attachment"], &attachments)
if err != nil {
return err
}
sort.Sort(&attachments)
if err := opts.PrintTemplate(attachments); err != nil {
return err
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
jira "gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type AttachRemoveOptions struct {
AttachmentID string `yaml:"attachment-id,omitempty" json:"attachment-id,omitempty"`
}
func CmdAttachRemoveRegistry() *jiracli.CommandRegistryEntry {
opts := AttachRemoveOptions{}
return &jiracli.CommandRegistryEntry{
"Delete attachment",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdAttachRemoveUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdAttachRemove(o, globals, &opts)
},
}
}
func CmdAttachRemoveUsage(cmd *kingpin.CmdClause, opts *AttachRemoveOptions) error {
cmd.Arg("ATTACHMENT-ID", "Attachment id to fetch").StringVar(&opts.AttachmentID)
return nil
}
func CmdAttachRemove(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AttachRemoveOptions) error {
if err := jira.RemoveAttachment(o, globals.Endpoint.Value, opts.AttachmentID); err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK Deleted Attachment %s\n", opts.AttachmentID)
}
return nil
}
+80
View File
@@ -0,0 +1,80 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type BlockOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
jiradata.LinkIssueRequest `yaml:",inline" json:",inline" figtree:",inline"`
}
func CmdBlockRegistry() *jiracli.CommandRegistryEntry {
opts := BlockOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("edit"),
},
LinkIssueRequest: jiradata.LinkIssueRequest{
Type: &jiradata.IssueLinkType{
Name: "Blocks",
},
InwardIssue: &jiradata.IssueRef{},
OutwardIssue: &jiradata.IssueRef{},
},
}
return &jiracli.CommandRegistryEntry{
"Mark issues as blocker",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdBlockUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdBlock(o, globals, &opts)
},
}
}
func CmdBlockUsage(cmd *kingpin.CmdClause, opts *BlockOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.EditorUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("comment", "Comment message when marking issue as blocker").Short('m').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Comment = &jiradata.Comment{
Body: jiracli.FlagValue(ctx, "comment"),
}
return nil
}).String()
cmd.Arg("BLOCKER", "blocker issue").Required().StringVar(&opts.OutwardIssue.Key)
cmd.Arg("ISSUE", "issue that is blocked").Required().StringVar(&opts.InwardIssue.Key)
return nil
}
// CmdBlock will update the given issue as being a duplicate by the given dup issue
// and will attempt to resolve the dup issue
func CmdBlock(o *oreo.Client, globals *jiracli.GlobalOptions, opts *BlockOptions) error {
if err := jira.LinkIssues(o, globals.Endpoint.Value, &opts.LinkIssueRequest); err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.InwardIssue.Key, globals.Endpoint.Value, opts.InwardIssue.Key)
fmt.Printf("OK %s %s/browse/%s\n", opts.OutwardIssue.Key, globals.Endpoint.Value, opts.OutwardIssue.Key)
}
if opts.Browse.Value {
if err := CmdBrowse(globals, opts.InwardIssue.Key); err != nil {
return CmdBrowse(globals, opts.OutwardIssue.Key)
}
}
return nil
}
+31
View File
@@ -0,0 +1,31 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"github.com/pkg/browser"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
func CmdBrowseRegistry() *jiracli.CommandRegistryEntry {
issue := ""
return &jiracli.CommandRegistryEntry{
"Open issue in browser",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
cmd.Arg("ISSUE", "Issue to browse to").Required().StringVar(&issue)
return nil
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdBrowse(globals, issue)
},
}
}
// CmdBrowse open the default system browser to the provided issue
func CmdBrowse(globals *jiracli.GlobalOptions, issue string) error {
return browser.OpenURL(fmt.Sprintf("%s/browse/%s", globals.Endpoint.Value, issue))
}
+79
View File
@@ -0,0 +1,79 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type CommentOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Overrides map[string]string `yaml:"overrides,omitempty" json:"overrides,omitempty"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
}
func CmdCommentRegistry() *jiracli.CommandRegistryEntry {
opts := CommentOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("comment"),
},
Overrides: map[string]string{},
}
return &jiracli.CommandRegistryEntry{
"Add comment to issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdCommentUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdComment(o, globals, &opts)
},
}
}
func CmdCommentUsage(cmd *kingpin.CmdClause, opts *CommentOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.EditorUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("noedit", "Disable opening the editor").SetValue(&opts.SkipEditing)
cmd.Flag("comment", "Comment message for issue").Short('m').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Overrides["comment"] = jiracli.FlagValue(ctx, "comment")
return nil
}).String()
cmd.Arg("ISSUE", "issue id to update").StringVar(&opts.Issue)
return nil
}
// CmdComment will update issue with comment
func CmdComment(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CommentOptions) error {
comment := jiradata.Comment{}
input := struct {
Overrides map[string]string `yaml:"overrides,omitempty" json:"overrides,omitempty"`
}{
opts.Overrides,
}
err := jiracli.EditLoop(&opts.CommonOptions, &input, &comment, func() error {
_, err := jira.IssueAddComment(o, globals.Endpoint.Value, opts.Issue, &comment)
return err
})
if err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+68
View File
@@ -0,0 +1,68 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type ComponentAddOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
jiradata.Component `yaml:",inline" json:",inline" figtree:",inline"`
}
func CmdComponentAddRegistry() *jiracli.CommandRegistryEntry {
opts := ComponentAddOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("component-add"),
},
}
return &jiracli.CommandRegistryEntry{
"Add component",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdComponentAddUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdComponentAdd(o, globals, &opts)
},
}
}
func CmdComponentAddUsage(cmd *kingpin.CmdClause, opts *ComponentAddOptions) error {
jiracli.EditorUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("noedit", "Disable opening the editor").SetValue(&opts.SkipEditing)
cmd.Flag("project", "project to create component in").Short('p').StringVar(&opts.Project)
cmd.Flag("name", "name of component").Short('n').StringVar(&opts.Name)
cmd.Flag("description", "description of component").Short('d').StringVar(&opts.Description)
cmd.Flag("lead", "person that acts as lead for component").Short('l').StringVar(&opts.LeadUserName)
return nil
}
// CmdComponentAdd sends the provided overrides to the "component-add" template for editing, then
// will parse the edited document as YAML and submit the document to jira.
func CmdComponentAdd(o *oreo.Client, globals *jiracli.GlobalOptions, opts *ComponentAddOptions) error {
var err error
component := &jiradata.Component{}
var resp *jiradata.Component
err = jiracli.EditLoop(&opts.CommonOptions, &opts.Component, component, func() error {
resp, err = jira.CreateComponent(o, globals.Endpoint.Value, component)
return err
})
if err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s\n", component.Project, component.Name)
}
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type ComponentsOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Project string `yaml:"project,omitempty" json:"project,omitempty"`
}
func CmdComponentsRegistry() *jiracli.CommandRegistryEntry {
opts := ComponentsOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("components"),
},
}
return &jiracli.CommandRegistryEntry{
"Show components for a project",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdComponentsUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdComponents(o, globals, &opts)
},
}
}
func CmdComponentsUsage(cmd *kingpin.CmdClause, opts *ComponentsOptions) error {
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
jiracli.GJsonQueryUsage(cmd, &opts.CommonOptions)
cmd.Flag("project", "project to list components").Short('p').StringVar(&opts.Project)
return nil
}
// CmdComponents will get available components for project and send to the "components" template
func CmdComponents(o *oreo.Client, globals *jiracli.GlobalOptions, opts *ComponentsOptions) error {
if opts.Project == "" {
return fmt.Errorf("Project Required.")
}
data, err := jira.GetProjectComponents(o, globals.Endpoint.Value, opts.Project)
if err != nil {
return err
}
return opts.PrintTemplate(data)
}
+152
View File
@@ -0,0 +1,152 @@
package jiracmd
import (
"fmt"
"os"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
yaml "gopkg.in/coryb/yaml.v2"
)
type CreateOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
jiradata.IssueUpdate `yaml:",inline" json:",inline" figtree:",inline"`
Project string `yaml:"project,omitempty" json:"project,omitempty"`
IssueType string `yaml:"issuetype,omitempty" json:"issuetype,omitempty"`
Overrides map[string]string `yaml:"overrides,omitempty" json:"overrides,omitempty"`
SaveFile string `yaml:"savefile,omitempty" json:"savefile,omitempty"`
}
func CmdCreateRegistry() *jiracli.CommandRegistryEntry {
opts := CreateOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("create"),
},
Overrides: map[string]string{},
}
return &jiracli.CommandRegistryEntry{
"Create issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdCreateUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdCreate(o, globals, &opts)
},
}
}
func CmdCreateUsage(cmd *kingpin.CmdClause, opts *CreateOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.EditorUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("noedit", "Disable opening the editor").SetValue(&opts.SkipEditing)
cmd.Flag("project", "project to create issue in").Short('p').StringVar(&opts.Project)
cmd.Flag("issuetype", "issuetype in to create").Short('i').StringVar(&opts.IssueType)
cmd.Flag("comment", "Comment message for issue").Short('m').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Overrides["comment"] = jiracli.FlagValue(ctx, "comment")
return nil
}).String()
cmd.Flag("override", "Set issue property").Short('o').StringMapVar(&opts.Overrides)
cmd.Flag("saveFile", "Write issue as yaml to file").StringVar(&opts.SaveFile)
return nil
}
// CmdCreate sends the create-metadata to the "create" template for editing, then
// will parse the edited document as YAML and submit the document to jira.
func CmdCreate(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CreateOptions) error {
type templateInput struct {
Meta *jiradata.IssueType `yaml:"meta" json:"meta"`
Overrides map[string]string `yaml:"overrides" json:"overrides"`
}
if err := defaultIssueType(o, globals.Endpoint.Value, &opts.Project, &opts.IssueType); err != nil {
return err
}
createMeta, err := jira.GetIssueCreateMetaIssueType(o, globals.Endpoint.Value, opts.Project, opts.IssueType)
if err != nil {
return err
}
issueUpdate := jiradata.IssueUpdate{}
input := templateInput{
Meta: createMeta,
Overrides: opts.Overrides,
}
input.Overrides["project"] = opts.Project
input.Overrides["issuetype"] = opts.IssueType
input.Overrides["user"] = globals.User.Value
var issueResp *jiradata.IssueCreateResponse
err = jiracli.EditLoop(&opts.CommonOptions, &input, &issueUpdate, func() error {
issueResp, err = jira.CreateIssue(o, globals.Endpoint.Value, &issueUpdate)
return err
})
if err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", issueResp.Key, globals.Endpoint.Value, issueResp.Key)
}
if opts.SaveFile != "" {
fh, err := os.Create(opts.SaveFile)
if err != nil {
return err
}
defer fh.Close()
out, err := yaml.Marshal(map[string]string{
"issue": issueResp.Key,
"link": fmt.Sprintf("%s/browse/%s", globals.Endpoint.Value, issueResp.Key),
})
if err != nil {
return err
}
fh.Write(out)
}
if opts.Browse.Value {
return CmdBrowse(globals, issueResp.Key)
}
return nil
}
func defaultIssueType(o *oreo.Client, endpoint string, project, issuetype *string) error {
if project == nil || *project == "" {
return fmt.Errorf("Project undefined, please use --project argument or set the `project` config property")
}
if issuetype != nil && *issuetype != "" {
return nil
}
projectMeta, err := jira.GetIssueCreateMetaProject(o, endpoint, *project)
if err != nil {
return err
}
issueTypes := map[string]bool{}
for _, issuetype := range projectMeta.IssueTypes {
issueTypes[issuetype.Name] = true
}
// prefer "Bug" type
if _, ok := issueTypes["Bug"]; ok {
*issuetype = "Bug"
return nil
}
// next best default it "Task"
if _, ok := issueTypes["Task"]; ok {
*issuetype = "Task"
return nil
}
return fmt.Errorf("Unable to find default issueType of Bug or Task, please set --issuetype argument or set the `issuetype` config property")
}
+54
View File
@@ -0,0 +1,54 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type CreateMetaOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Project string `yaml:"project,omitempty" json:"project,omitempty"`
IssueType string `yaml:"issuetype,omitempty" json:"issuetype,omitempty"`
}
func CmdCreateMetaRegistry() *jiracli.CommandRegistryEntry {
opts := CreateMetaOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("createmeta"),
},
}
return &jiracli.CommandRegistryEntry{
"View 'create' metadata",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdCreateMetaUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdCreateMeta(o, globals, &opts)
},
}
}
func CmdCreateMetaUsage(cmd *kingpin.CmdClause, opts *CreateMetaOptions) error {
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
jiracli.GJsonQueryUsage(cmd, &opts.CommonOptions)
cmd.Flag("project", "project to fetch create metadata").Short('p').StringVar(&opts.Project)
cmd.Flag("issuetype", "issuetype in project to fetch create metadata").Short('i').StringVar(&opts.IssueType)
return nil
}
// Create will get issue create metadata and send to "createmeta" template
func CmdCreateMeta(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CreateMetaOptions) error {
if err := defaultIssueType(o, globals.Endpoint.Value, &opts.Project, &opts.IssueType); err != nil {
return err
}
createMeta, err := jira.GetIssueCreateMetaIssueType(o, globals.Endpoint.Value, opts.Project, opts.IssueType)
if err != nil {
return err
}
return opts.PrintTemplate(createMeta)
}
+116
View File
@@ -0,0 +1,116 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type DupOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
jiradata.LinkIssueRequest `yaml:",inline" json:",inline" figtree:",inline"`
Duplicate string `yaml:"duplicate,omitempty" json:"duplicate,omitempty"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
}
func CmdDupRegistry() *jiracli.CommandRegistryEntry {
opts := DupOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("edit"),
},
LinkIssueRequest: jiradata.LinkIssueRequest{
Type: &jiradata.IssueLinkType{
Name: "Duplicate",
},
InwardIssue: &jiradata.IssueRef{},
OutwardIssue: &jiradata.IssueRef{},
},
}
return &jiracli.CommandRegistryEntry{
"Mark issues as duplicate",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdDupUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdDup(o, globals, &opts)
},
}
}
func CmdDupUsage(cmd *kingpin.CmdClause, opts *DupOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.EditorUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("comment", "Comment message when marking issue as duplicate").Short('m').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Comment = &jiradata.Comment{
Body: jiracli.FlagValue(ctx, "comment"),
}
return nil
}).String()
cmd.Arg("DUPLICATE", "duplicate issue to mark closed").Required().StringVar(&opts.InwardIssue.Key)
cmd.Arg("ISSUE", "duplicate issue to leave open").Required().StringVar(&opts.OutwardIssue.Key)
return nil
}
// CmdDups will update the given issue as being a duplicate by the given dup issue
// and will attempt to resolve the dup issue
func CmdDup(o *oreo.Client, globals *jiracli.GlobalOptions, opts *DupOptions) error {
if err := jira.LinkIssues(o, globals.Endpoint.Value, &opts.LinkIssueRequest); err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.OutwardIssue.Key, globals.Endpoint.Value, opts.OutwardIssue.Key)
}
meta, err := jira.GetIssueTransitions(o, globals.Endpoint.Value, opts.InwardIssue.Key)
if err != nil {
return err
}
for _, trans := range []string{"close", "done", "cancel", "start", "stop"} {
transMeta := meta.Transitions.Find(trans)
if transMeta != nil {
issueUpdate := jiradata.IssueUpdate{
Transition: transMeta,
}
resolution := defaultResolution(transMeta)
if resolution != "" {
issueUpdate.Fields = map[string]interface{}{
"resolution": map[string]interface{}{
"name": resolution,
},
}
}
if err = jira.TransitionIssue(o, globals.Endpoint.Value, opts.InwardIssue.Key, &issueUpdate); err != nil {
return err
}
if trans != "start" {
break
}
// if we are here then we must be stopping, so need to reset the meta
meta, err = jira.GetIssueTransitions(o, globals.Endpoint.Value, opts.InwardIssue.Key)
if err != nil {
return err
}
}
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.InwardIssue.Key, globals.Endpoint.Value, opts.InwardIssue.Key)
}
if opts.Browse.Value {
if err := CmdBrowse(globals, opts.OutwardIssue.Key); err != nil {
return CmdBrowse(globals, opts.InwardIssue.Key)
}
}
return nil
}
+133
View File
@@ -0,0 +1,133 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type EditOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
jiradata.IssueUpdate `yaml:",inline" json:",inline" figtree:",inline"`
jira.SearchOptions `yaml:",inline" json:",inline" figtree:",inline"`
Overrides map[string]string `yaml:"overrides,omitempty" json:"overrides,omitempty"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
Queries map[string]string `yaml:"queries,omitempty" json:"queries,omitempty"`
}
func CmdEditRegistry() *jiracli.CommandRegistryEntry {
opts := EditOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("edit"),
},
Overrides: map[string]string{},
}
return &jiracli.CommandRegistryEntry{
"Edit issue details",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdEditUsage(cmd, &opts, fig)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdEdit(o, globals, &opts)
},
}
}
func CmdEditUsage(cmd *kingpin.CmdClause, opts *EditOptions, fig *figtree.FigTree) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.EditorUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("noedit", "Disable opening the editor").SetValue(&opts.SkipEditing)
cmd.Flag("named-query", "The name of a query in the `queries` configuration").Short('n').PreAction(func(ctx *kingpin.ParseContext) error {
name := jiracli.FlagValue(ctx, "named-query")
if query, ok := opts.Queries[name]; ok && query != "" {
var err error
opts.Query, err = jiracli.ConfigTemplate(fig, query, cmd.FullCommand(), opts)
return err
}
return fmt.Errorf("A valid named-query %q not found in `queries` configuration", name)
}).String()
cmd.Flag("query", "Jira Query Language (JQL) expression for the search to edit multiple issues").Short('q').StringVar(&opts.Query)
cmd.Flag("comment", "Comment message for issue").Short('m').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Overrides["comment"] = jiracli.FlagValue(ctx, "comment")
return nil
}).String()
cmd.Flag("override", "Set issue property").Short('o').StringMapVar(&opts.Overrides)
cmd.Arg("ISSUE", "issue id to edit").StringVar(&opts.Issue)
return nil
}
// Edit will get issue data and send to "edit" template
func CmdEdit(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EditOptions) error {
type templateInput struct {
*jiradata.Issue `yaml:",inline"`
Meta *jiradata.EditMeta `yaml:"meta" json:"meta"`
Overrides map[string]string `yaml:"overrides" json:"overrides"`
}
if opts.Issue != "" {
issueData, err := jira.GetIssue(o, globals.Endpoint.Value, opts.Issue, nil)
if err != nil {
return err
}
editMeta, err := jira.GetIssueEditMeta(o, globals.Endpoint.Value, opts.Issue)
if err != nil {
return err
}
issueUpdate := jiradata.IssueUpdate{}
input := templateInput{
Issue: issueData,
Meta: editMeta,
Overrides: opts.Overrides,
}
err = jiracli.EditLoop(&opts.CommonOptions, &input, &issueUpdate, func() error {
return jira.EditIssue(o, globals.Endpoint.Value, opts.Issue, &issueUpdate)
})
if err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
}
results, err := jira.Search(o, globals.Endpoint.Value, opts)
if err != nil {
return err
}
for _, issueData := range results.Issues {
editMeta, err := jira.GetIssueEditMeta(o, globals.Endpoint.Value, issueData.Key)
if err != nil {
return err
}
issueUpdate := jiradata.IssueUpdate{}
input := templateInput{
Issue: issueData,
Meta: editMeta,
}
err = jiracli.EditLoop(&opts.CommonOptions, &input, &issueUpdate, func() error {
return jira.EditIssue(o, globals.Endpoint.Value, issueData.Key, &issueUpdate)
})
if err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", issueData.Key, globals.Endpoint.Value, issueData.Key)
}
if opts.Browse.Value {
return CmdBrowse(globals, issueData.Key)
}
}
return nil
}
+57
View File
@@ -0,0 +1,57 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type EditMetaOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
}
func CmdEditMetaRegistry() *jiracli.CommandRegistryEntry {
opts := EditMetaOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("editmeta"),
},
}
return &jiracli.CommandRegistryEntry{
"View 'edit' metadata",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdEditMetaUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdEditMeta(o, globals, &opts)
},
}
}
func CmdEditMetaUsage(cmd *kingpin.CmdClause, opts *EditMetaOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
jiracli.GJsonQueryUsage(cmd, &opts.CommonOptions)
cmd.Arg("ISSUE", "edit metadata for issue id").Required().StringVar(&opts.Issue)
return nil
}
// EditMeta will get issue edit metadata and send to "editmeta" template
func CmdEditMeta(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EditMetaOptions) error {
editMeta, err := jira.GetIssueEditMeta(o, globals.Endpoint.Value, opts.Issue)
if err != nil {
return err
}
if err := opts.PrintTemplate(editMeta); err != nil {
return err
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+54
View File
@@ -0,0 +1,54 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type EpicAddOptions struct {
jiradata.EpicIssues `yaml:",inline" json:",inline" figtree:",inline"`
Epic string `yaml:"epic,omitempty" json:"epic,omitempty"`
}
func CmdEpicAddRegistry() *jiracli.CommandRegistryEntry {
opts := EpicAddOptions{}
return &jiracli.CommandRegistryEntry{
"Add issues to Epic",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdEpicAddUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdEpicAdd(o, globals, &opts)
},
}
}
func CmdEpicAddUsage(cmd *kingpin.CmdClause, opts *EpicAddOptions) error {
cmd.Arg("EPIC", "Epic Key or ID to add issues to").Required().StringVar(&opts.Epic)
cmd.Arg("ISSUE", "Issues to add to epic").Required().StringsVar(&opts.Issues)
return nil
}
func CmdEpicAdd(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EpicAddOptions) error {
if err := jira.EpicAddIssues(o, globals.Endpoint.Value, opts.Epic, &opts.EpicIssues); err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.Epic, globals.Endpoint.Value, opts.Epic)
for _, issue := range opts.Issues {
fmt.Printf("OK %s %s/browse/%s\n", issue, globals.Endpoint.Value, issue)
}
}
return nil
}
+48
View File
@@ -0,0 +1,48 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
func CmdEpicCreateRegistry() *jiracli.CommandRegistryEntry {
opts := CreateOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("epic-create"),
},
Overrides: map[string]string{},
}
return &jiracli.CommandRegistryEntry{
"Create Epic",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdEpicCreateUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdCreate(o, globals, &opts)
},
}
}
func CmdEpicCreateUsage(cmd *kingpin.CmdClause, opts *CreateOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.EditorUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("noedit", "Disable opening the editor").SetValue(&opts.SkipEditing)
cmd.Flag("project", "project to create epic in").Short('p').StringVar(&opts.Project)
cmd.Flag("epic-name", "Epic Name").Short('n').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Overrides["epic-name"] = jiracli.FlagValue(ctx, "epic-name")
return nil
}).String()
cmd.Flag("comment", "Comment message for epic").Short('m').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Overrides["comment"] = jiracli.FlagValue(ctx, "comment")
return nil
}).String()
cmd.Flag("override", "Set epic property").Short('o').StringMapVar(&opts.Overrides)
cmd.Flag("saveFile", "Write epic as yaml to file").StringVar(&opts.SaveFile)
return nil
}
+58
View File
@@ -0,0 +1,58 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type EpicListOptions struct {
ListOptions `yaml:",inline" json:",inline" figtree:",inline"`
Epic string `yaml:"epic,omitempty" json:"epic,omitempty"`
}
func CmdEpicListRegistry() *jiracli.CommandRegistryEntry {
opts := EpicListOptions{
ListOptions: ListOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("epic-list"),
},
},
}
return &jiracli.CommandRegistryEntry{
"Prints list of issues for an epic with optional search criteria",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
if opts.MaxResults == 0 {
opts.MaxResults = 500
}
if opts.QueryFields == "" {
opts.QueryFields = "assignee,created,priority,reporter,status,summary,updated,issuetype"
}
if opts.Sort == "" {
opts.Sort = "priority asc, key"
}
return CmdEpicListUsage(cmd, &opts, fig)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdEpicList(o, globals, &opts)
},
}
}
func CmdEpicListUsage(cmd *kingpin.CmdClause, opts *EpicListOptions, fig *figtree.FigTree) error {
CmdListUsage(cmd, &opts.ListOptions, fig)
cmd.Arg("EPIC", "Epic Key or ID to list").Required().StringVar(&opts.Epic)
return nil
}
func CmdEpicList(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EpicListOptions) error {
data, err := jira.EpicSearch(o, globals.Endpoint.Value, opts.Epic, opts)
if err != nil {
return err
}
return opts.PrintTemplate(data)
}
+51
View File
@@ -0,0 +1,51 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type EpicRemoveOptions struct {
jiradata.EpicIssues `yaml:",inline" json:",inline" figtree:",inline"`
}
func CmdEpicRemoveRegistry() *jiracli.CommandRegistryEntry {
opts := EpicRemoveOptions{}
return &jiracli.CommandRegistryEntry{
"Remove issues from Epic",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdEpicRemoveUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdEpicRemove(o, globals, &opts)
},
}
}
func CmdEpicRemoveUsage(cmd *kingpin.CmdClause, opts *EpicRemoveOptions) error {
cmd.Arg("ISSUE", "Issues to remove from any epic").Required().StringsVar(&opts.Issues)
return nil
}
func CmdEpicRemove(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EpicRemoveOptions) error {
if err := jira.EpicRemoveIssues(o, globals.Endpoint.Value, &opts.EpicIssues); err != nil {
return err
}
if !globals.Quiet.Value {
for _, issue := range opts.Issues {
fmt.Printf("OK %s %s/browse/%s\n", issue, globals.Endpoint.Value, issue)
}
}
return nil
}
+71
View File
@@ -0,0 +1,71 @@
package jiracmd
import (
"fmt"
"os"
"path"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type ExportTemplatesOptions struct {
Template string `yaml:"template,omitempty" json:"template,omitempty"`
Dir string `yaml:"dir,omitempty" json:"dir,omitempty"`
}
func CmdExportTemplatesRegistry() *jiracli.CommandRegistryEntry {
opts := ExportTemplatesOptions{}
return &jiracli.CommandRegistryEntry{
"Export templates for customizations",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
if opts.Dir == "" {
opts.Dir = fmt.Sprintf("%s/.jira.d/templates", jiracli.Homedir())
}
return CmdExportTemplatesUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdExportTemplates(globals, &opts)
},
}
}
func CmdExportTemplatesUsage(cmd *kingpin.CmdClause, opts *ExportTemplatesOptions) error {
cmd.Flag("template", "Template to export").Short('t').StringVar(&opts.Template)
cmd.Flag("dir", "directory to write tempates to").Short('d').StringVar(&opts.Dir)
return nil
}
// CmdExportTemplates will export templates to directory
func CmdExportTemplates(globals *jiracli.GlobalOptions, opts *ExportTemplatesOptions) error {
if err := os.MkdirAll(opts.Dir, 0755); err != nil {
return err
}
for name, template := range jiracli.AllTemplates {
if opts.Template != "" && opts.Template != name {
continue
}
templateFile := path.Join(opts.Dir, name)
if _, err := os.Stat(templateFile); err == nil {
log.Warning("Skipping %s, already exists", templateFile)
continue
}
fh, err := os.OpenFile(templateFile, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
log.Errorf("Failed to open %s for writing: %s", templateFile, err)
return err
}
defer fh.Close()
if !globals.Quiet.Value {
log.Noticef("Creating %s", templateFile)
}
fh.Write([]byte(template))
}
return nil
}
+36
View File
@@ -0,0 +1,36 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
func CmdFieldsRegistry() *jiracli.CommandRegistryEntry {
opts := jiracli.CommonOptions{
Template: figtree.NewStringOption("fields"),
}
return &jiracli.CommandRegistryEntry{
"Prints all fields, both System and Custom",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
jiracli.TemplateUsage(cmd, &opts)
jiracli.GJsonQueryUsage(cmd, &opts)
return nil
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdFields(o, globals, &opts)
},
}
}
// Fields will send data from /rest/api/2/field API to "fields" template
func CmdFields(o *oreo.Client, globals *jiracli.GlobalOptions, opts *jiracli.CommonOptions) error {
data, err := jira.GetFields(o, globals.Endpoint.Value)
if err != nil {
return err
}
return opts.PrintTemplate(data)
}
+76
View File
@@ -0,0 +1,76 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type IssueLinkOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
jiradata.LinkIssueRequest `yaml:",inline" json:",inline" figtree:",inline"`
LinkType string `yaml:"linktype,omitempty" json:"linktype,omitempty"`
}
func CmdIssueLinkRegistry() *jiracli.CommandRegistryEntry {
opts := IssueLinkOptions{
LinkIssueRequest: jiradata.LinkIssueRequest{
Type: &jiradata.IssueLinkType{},
InwardIssue: &jiradata.IssueRef{},
OutwardIssue: &jiradata.IssueRef{},
},
}
return &jiracli.CommandRegistryEntry{
"Link two issues",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdIssueLinkUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdIssueLink(o, globals, &opts)
},
}
}
func CmdIssueLinkUsage(cmd *kingpin.CmdClause, opts *IssueLinkOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.EditorUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("comment", "Comment message when linking issue").Short('m').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Comment = &jiradata.Comment{
Body: jiracli.FlagValue(ctx, "comment"),
}
return nil
}).String()
cmd.Arg("OUTWARDISSUE", "outward issue").Required().StringVar(&opts.OutwardIssue.Key)
cmd.Arg("ISSUELINKTYPE", "issue link type").Required().StringVar(&opts.Type.Name)
cmd.Arg("INWARDISSUE", "inward issue").Required().StringVar(&opts.InwardIssue.Key)
return nil
}
// CmdBlock will update the given issue as being a duplicate by the given dup issue
// and will attempt to resolve the dup issue
func CmdIssueLink(o *oreo.Client, globals *jiracli.GlobalOptions, opts *IssueLinkOptions) error {
if err := jira.LinkIssues(o, globals.Endpoint.Value, &opts.LinkIssueRequest); err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.InwardIssue.Key, globals.Endpoint.Value, opts.InwardIssue.Key)
fmt.Printf("OK %s %s/browse/%s\n", opts.OutwardIssue.Key, globals.Endpoint.Value, opts.OutwardIssue.Key)
}
if opts.Browse.Value {
if err := CmdBrowse(globals, opts.OutwardIssue.Key); err != nil {
return CmdBrowse(globals, opts.InwardIssue.Key)
}
}
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
func CmdIssueLinkTypesRegistry() *jiracli.CommandRegistryEntry {
opts := jiracli.CommonOptions{
Template: figtree.NewStringOption("issuelinktypes"),
}
return &jiracli.CommandRegistryEntry{
"Show the issue link types",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdIssueLinkTypesUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdIssueLinkTypes(o, globals, &opts)
},
}
}
func CmdIssueLinkTypesUsage(cmd *kingpin.CmdClause, opts *jiracli.CommonOptions) error {
jiracli.TemplateUsage(cmd, opts)
jiracli.GJsonQueryUsage(cmd, opts)
return nil
}
// CmdIssueLinkTypes will get issue link type data and send to "issuelinktypes" template
func CmdIssueLinkTypes(o *oreo.Client, globals *jiracli.GlobalOptions, opts *jiracli.CommonOptions) error {
data, err := jira.GetIssueLinkTypes(o, globals.Endpoint.Value)
if err != nil {
return err
}
return opts.PrintTemplate(data)
}
+56
View File
@@ -0,0 +1,56 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type IssueTypesOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Project string `yaml:"project,omitempty" json:"project,omitempty"`
}
func CmdIssueTypesRegistry() *jiracli.CommandRegistryEntry {
opts := IssueTypesOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("issuetypes"),
},
}
return &jiracli.CommandRegistryEntry{
"Show issue types for a project",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdIssueTypesUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdIssueTypes(o, globals, &opts)
},
}
}
func CmdIssueTypesUsage(cmd *kingpin.CmdClause, opts *IssueTypesOptions) error {
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
jiracli.GJsonQueryUsage(cmd, &opts.CommonOptions)
cmd.Flag("project", "project to list issueTypes").Short('p').StringVar(&opts.Project)
return nil
}
// CmdIssueTypes will get available issueTypes for project and send to the "issueTypes" template
func CmdIssueTypes(o *oreo.Client, globals *jiracli.GlobalOptions, opts *IssueTypesOptions) error {
if opts.Project == "" {
return fmt.Errorf("Project Required.")
}
data, err := jira.GetIssueCreateMetaProject(o, globals.Endpoint.Value, opts.Project)
if err != nil {
return err
}
return opts.PrintTemplate(data)
}
+66
View File
@@ -0,0 +1,66 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type LabelsAddOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"`
}
func CmdLabelsAddRegistry() *jiracli.CommandRegistryEntry {
opts := LabelsAddOptions{}
return &jiracli.CommandRegistryEntry{
"Add labels to an issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdLabelsAddUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdLabelsAdd(o, globals, &opts)
},
}
}
func CmdLabelsAddUsage(cmd *kingpin.CmdClause, opts *LabelsAddOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Arg("ISSUE", "issue id to modify labels").Required().StringVar(&opts.Issue)
cmd.Arg("LABEL", "label to add to issue").Required().StringsVar(&opts.Labels)
return nil
}
// CmdLabels will add labels on a given issue
func CmdLabelsAdd(o *oreo.Client, globals *jiracli.GlobalOptions, opts *LabelsAddOptions) error {
ops := jiradata.FieldOperations{}
for _, label := range opts.Labels {
ops = append(ops, jiradata.FieldOperation{
"add": label,
})
}
issueUpdate := jiradata.IssueUpdate{
Update: jiradata.FieldOperationsMap{
"labels": ops,
},
}
if err := jira.EditIssue(o, globals.Endpoint.Value, opts.Issue, &issueUpdate); err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+67
View File
@@ -0,0 +1,67 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type LabelsRemoveOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"`
}
func CmdLabelsRemoveRegistry() *jiracli.CommandRegistryEntry {
opts := LabelsRemoveOptions{}
return &jiracli.CommandRegistryEntry{
"Remove labels from an issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdLabelsRemoveUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdLabelsRemove(o, globals, &opts)
},
}
}
func CmdLabelsRemoveUsage(cmd *kingpin.CmdClause, opts *LabelsRemoveOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Arg("ISSUE", "issue id to modify labels").Required().StringVar(&opts.Issue)
cmd.Arg("LABEL", "label to remove from issue").Required().StringsVar(&opts.Labels)
return nil
}
// CmdLabels will remove labels on a given issue
func CmdLabelsRemove(o *oreo.Client, globals *jiracli.GlobalOptions, opts *LabelsRemoveOptions) error {
ops := jiradata.FieldOperations{}
for _, label := range opts.Labels {
ops = append(ops, jiradata.FieldOperation{
"remove": label,
})
}
issueUpdate := jiradata.IssueUpdate{
Update: jiradata.FieldOperationsMap{
"labels": ops,
},
}
err := jira.EditIssue(o, globals.Endpoint.Value, opts.Issue, &issueUpdate)
if err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+64
View File
@@ -0,0 +1,64 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type LabelsSetOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
Labels []string `yaml:"labels,omitempty" json:"labels,omitempty"`
}
func CmdLabelsSetRegistry() *jiracli.CommandRegistryEntry {
opts := LabelsSetOptions{}
return &jiracli.CommandRegistryEntry{
"Set labels on an issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdLabelsSetUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdLabelsSet(o, globals, &opts)
},
}
}
func CmdLabelsSetUsage(cmd *kingpin.CmdClause, opts *LabelsSetOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Arg("ISSUE", "issue id to modify labels").Required().StringVar(&opts.Issue)
cmd.Arg("LABEL", "label to set on issue").Required().StringsVar(&opts.Labels)
return nil
}
// CmdLabels will set labels on a given issue
func CmdLabelsSet(o *oreo.Client, globals *jiracli.GlobalOptions, opts *LabelsSetOptions) error {
issueUpdate := jiradata.IssueUpdate{
Update: jiradata.FieldOperationsMap{
"labels": jiradata.FieldOperations{
jiradata.FieldOperation{
"set": opts.Labels,
},
},
},
}
if err := jira.EditIssue(o, globals.Endpoint.Value, opts.Issue, &issueUpdate); err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+80
View File
@@ -0,0 +1,80 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type ListOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
jira.SearchOptions `yaml:",inline" json:",inline" figtree:",inline"`
Queries map[string]string `yaml:"queries,omitempty" json:"queries,omitempty"`
}
func CmdListRegistry() *jiracli.CommandRegistryEntry {
opts := ListOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("list"),
},
}
return &jiracli.CommandRegistryEntry{
"Prints list of issues for given search criteria",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
if opts.MaxResults == 0 {
opts.MaxResults = 500
}
if opts.QueryFields == "" {
opts.QueryFields = "assignee,created,priority,reporter,status,summary,updated,issuetype"
}
if opts.Sort == "" {
opts.Sort = "priority asc, key"
}
return CmdListUsage(cmd, &opts, fig)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdList(o, globals, &opts)
},
}
}
func CmdListUsage(cmd *kingpin.CmdClause, opts *ListOptions, fig *figtree.FigTree) error {
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
jiracli.GJsonQueryUsage(cmd, &opts.CommonOptions)
cmd.Flag("assignee", "User assigned the issue").Short('a').StringVar(&opts.Assignee)
cmd.Flag("component", "Component to search for").Short('c').StringVar(&opts.Component)
cmd.Flag("issuetype", "Issue type to search for").Short('i').StringVar(&opts.IssueType)
cmd.Flag("limit", "Maximum number of results to return in search").Short('l').IntVar(&opts.MaxResults)
cmd.Flag("project", "Project to search for").Short('p').StringVar(&opts.Project)
cmd.Flag("named-query", "The name of a query in the `queries` configuration").Short('n').PreAction(func(ctx *kingpin.ParseContext) error {
name := jiracli.FlagValue(ctx, "named-query")
if query, ok := opts.Queries[name]; ok && query != "" {
var err error
opts.Query, err = jiracli.ConfigTemplate(fig, query, cmd.FullCommand(), opts)
return err
}
return fmt.Errorf("A valid named-query %q not found in `queries` configuration", name)
}).String()
cmd.Flag("query", "Jira Query Language (JQL) expression for the search").Short('q').StringVar(&opts.Query)
cmd.Flag("queryfields", "Fields that are used in \"list\" template").Short('f').StringVar(&opts.QueryFields)
cmd.Flag("reporter", "Reporter to search for").Short('r').StringVar(&opts.Reporter)
cmd.Flag("status", "Filter on issue status").Short('S').StringVar(&opts.Status)
cmd.Flag("sort", "Sort order to return").Short('s').StringVar(&opts.Sort)
cmd.Flag("watcher", "Watcher to search for").Short('w').StringVar(&opts.Watcher)
return nil
}
// List will query jira and send data to "list" template
func CmdList(o *oreo.Client, globals *jiracli.GlobalOptions, opts *ListOptions) error {
data, err := jira.Search(o, globals.Endpoint.Value, opts)
if err != nil {
return err
}
return opts.PrintTemplate(data)
}
+5
View File
@@ -0,0 +1,5 @@
package jiracmd
import logging "gopkg.in/op/go-logging.v1"
var log = logging.MustGetLogger("jiracmd")
+72
View File
@@ -0,0 +1,72 @@
package jiracmd
import (
"fmt"
"net/http"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"github.com/mgutz/ansi"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
func CmdLoginRegistry() *jiracli.CommandRegistryEntry {
opts := jiracli.CommonOptions{}
return &jiracli.CommandRegistryEntry{
"Attempt to login into jira server",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return nil
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdLogin(o, globals, &opts)
},
}
}
func authCallback(req *http.Request, resp *http.Response) (*http.Response, error) {
if resp.StatusCode == 403 {
defer resp.Body.Close()
// X-Authentication-Denied-Reason: CAPTCHA_CHALLENGE; login-url=https://jira/login.jsp
if reason := resp.Header.Get("X-Authentication-Denied-Reason"); reason != "" {
return resp, fmt.Errorf("Authenticaion Failed: " + reason)
}
return resp, fmt.Errorf("Authenticaion Failed: Unkown Reason")
} else if resp.StatusCode == 200 {
if reason := resp.Header.Get("X-Seraph-Loginreason"); reason == "AUTHENTICATION_DENIED" {
defer resp.Body.Close()
return resp, fmt.Errorf("Authentication Failed: " + reason)
}
}
return resp, nil
}
// CmdLogin will attempt to login into jira server
func CmdLogin(o *oreo.Client, globals *jiracli.GlobalOptions, opts *jiracli.CommonOptions) error {
ua := o.WithoutRedirect().WithRetries(0).WithoutCallbacks().WithPostCallback(authCallback)
for {
if session, err := jira.GetSession(o, globals.Endpoint.Value); err != nil {
// No active session so try to create a new one
_, err := jira.NewSession(ua, globals.Endpoint.Value, globals)
if err != nil {
// reset password on failed session
globals.SetPass("")
log.Errorf("%s", err)
continue
}
if !globals.Quiet.Value {
fmt.Println(ansi.Color("OK", "green"), "New session for", globals.User)
}
break
} else {
if !globals.Quiet.Value {
fmt.Println(ansi.Color("OK", "green"), "Found session for", session.Name)
}
break
}
}
return nil
}
+40
View File
@@ -0,0 +1,40 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"github.com/mgutz/ansi"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
func CmdLogoutRegistry() *jiracli.CommandRegistryEntry {
opts := jiracli.CommonOptions{}
return &jiracli.CommandRegistryEntry{
"Deactivate sesssion with Jira server",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return nil
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdLogout(o, globals, &opts)
},
}
}
// CmdLogout will attempt to terminate an active Jira session
func CmdLogout(o *oreo.Client, globals *jiracli.GlobalOptions, opts *jiracli.CommonOptions) error {
ua := o.WithoutRedirect().WithRetries(0).WithoutCallbacks()
err := jira.DeleteSession(ua, globals.Endpoint.Value)
if err == nil {
if !globals.Quiet.Value {
fmt.Println(ansi.Color("OK", "green"), "Terminated session for", globals.User)
}
} else {
fmt.Printf("%s Failed to terminate session for %s: %s", ansi.Color("ERROR", "red"), globals.User, err)
}
return nil
}
+73
View File
@@ -0,0 +1,73 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type RankOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
First string `yaml:"first,omitempty" json:"first,omitempty"`
Second string `yaml:"second,omitempty" json:"second,omitempty"`
Order string `yaml:"order,omitempty" json:"order,omitempty"`
}
func CmdRankRegistry() *jiracli.CommandRegistryEntry {
opts := RankOptions{}
return &jiracli.CommandRegistryEntry{
"Mark issues as blocker",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdRankUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdRank(o, globals, &opts)
},
}
}
func CmdRankUsage(cmd *kingpin.CmdClause, opts *RankOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Arg("FIRST-ISSUE", "first issue").Required().StringVar(&opts.First)
cmd.Arg("after|before", "rank ordering").Required().HintOptions("after", "before").EnumVar(&opts.Order, "after", "before")
cmd.Arg("SECOND-ISSUE", "second issue").Required().StringVar(&opts.Second)
return nil
}
// CmdRank order two issue
func CmdRank(o *oreo.Client, globals *jiracli.GlobalOptions, opts *RankOptions) error {
req := &jiradata.RankRequest{
Issues: []string{opts.First},
}
if opts.Order == "after" {
req.RankAfterIssue = opts.Second
} else {
req.RankBeforeIssue = opts.Second
}
if err := jira.RankIssues(o, globals.Endpoint.Value, req); err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.First, globals.Endpoint.Value, opts.First)
fmt.Printf("OK %s %s/browse/%s\n", opts.Second, globals.Endpoint.Value, opts.Second)
}
if opts.Browse.Value {
if err := CmdBrowse(globals, opts.First); err != nil {
return CmdBrowse(globals, opts.Second)
}
}
return nil
}
+95
View File
@@ -0,0 +1,95 @@
package jiracmd
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"strings"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type RequestOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Method string `yaml:"method,omitempty" json:"method,omitempty"`
URI string `yaml:"uri,omitempty" json:"uri,omitempty"`
Data string `yaml:"data,omitempty" json:"data,omitempty"`
}
func CmdRequestRegistry() *jiracli.CommandRegistryEntry {
opts := RequestOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("request"),
},
}
return &jiracli.CommandRegistryEntry{
"Open issue in requestr",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
if opts.Method == "" {
opts.Method = "GET"
}
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
jiracli.GJsonQueryUsage(cmd, &opts.CommonOptions)
return CmdRequestUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdRequest(o, globals, &opts)
},
}
}
func CmdRequestUsage(cmd *kingpin.CmdClause, opts *RequestOptions) error {
cmd.Flag("method", "HTTP request method to use").Short('M').EnumVar(&opts.Method, "GET", "PUT", "POST", "DELETE")
cmd.Arg("API", "Path to Jira API (ie: /rest/api/2/issue)").Required().StringVar(&opts.URI)
cmd.Arg("JSON", "JSON Content to send to API").StringVar(&opts.Data)
return nil
}
// CmdRequest open the default system requestr to the provided issue
func CmdRequest(o *oreo.Client, globals *jiracli.GlobalOptions, opts *RequestOptions) error {
uri := opts.URI
if !strings.HasPrefix(uri, "http") {
uri = globals.Endpoint.Value + uri
}
parsedURI, err := url.Parse(uri)
if err != nil {
return err
}
builder := oreo.RequestBuilder(parsedURI).WithMethod(opts.Method)
if opts.Data != "" {
builder = builder.WithJSON(opts.Data)
}
resp, err := o.Do(builder.Build())
if err != nil {
return err
}
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if len(content) == 0 {
if !globals.Quiet.Value {
fmt.Println("No content in response")
}
return nil
}
var data interface{}
err = json.Unmarshal(content, &data)
if err != nil {
return fmt.Errorf("JSON Parse Error: %s from %q", err, content)
}
return opts.PrintTemplate(&data)
}
+118
View File
@@ -0,0 +1,118 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type SubtaskOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
jiradata.IssueUpdate `yaml:",inline" json:",inline" figtree:",inline"`
Project string `yaml:"project,omitempty" json:"project,omitempty"`
IssueType string `yaml:"issuetype,omitempty" json:"issuetype,omitempty"`
Overrides map[string]string `yaml:"overrides,omitempty" json:"overrides,omitempty"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
}
func CmdSubtaskRegistry() *jiracli.CommandRegistryEntry {
opts := SubtaskOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("subtask"),
},
Overrides: map[string]string{},
}
return &jiracli.CommandRegistryEntry{
"Subtask issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
if opts.IssueType == "" {
opts.IssueType = "Sub-task"
}
return CmdSubtaskUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdSubtask(o, globals, &opts)
},
}
}
func CmdSubtaskUsage(cmd *kingpin.CmdClause, opts *SubtaskOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.EditorUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("noedit", "Disable opening the editor").SetValue(&opts.SkipEditing)
cmd.Flag("project", "project to subtask issue in").Short('p').StringVar(&opts.Project)
cmd.Flag("comment", "Comment message for issue").Short('m').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Overrides["comment"] = jiracli.FlagValue(ctx, "comment")
return nil
}).String()
cmd.Flag("override", "Set issue property").Short('o').StringMapVar(&opts.Overrides)
cmd.Arg("ISSUE", "Parent issue for subtask").StringVar(&opts.Issue)
return nil
}
// CmdSubtask sends the subtask-metadata to the "subtask" template for editing, then
// will parse the edited document as YAML and submit the document to jira.
func CmdSubtask(o *oreo.Client, globals *jiracli.GlobalOptions, opts *SubtaskOptions) error {
type templateInput struct {
Meta *jiradata.IssueType `yaml:"meta" json:"meta"`
Overrides map[string]string `yaml:"overrides" json:"overrides"`
Parent *jiradata.Issue `yaml:"parent" json:"parent"`
}
parent, err := jira.GetIssue(o, globals.Endpoint.Value, opts.Issue, nil)
if err != nil {
return err
}
if project, ok := parent.Fields["project"].(map[string]interface{}); ok {
if key, ok := project["key"].(string); ok {
opts.Project = key
} else {
return fmt.Errorf("Failed to find Project Key in parent issue")
}
} else {
return fmt.Errorf("Failed to find Project field in parent issue")
}
createMeta, err := jira.GetIssueCreateMetaIssueType(o, globals.Endpoint.Value, opts.Project, opts.IssueType)
if err != nil {
return err
}
issueUpdate := jiradata.IssueUpdate{}
input := templateInput{
Meta: createMeta,
Overrides: opts.Overrides,
Parent: parent,
}
input.Overrides["project"] = opts.Project
input.Overrides["issuetype"] = opts.IssueType
input.Overrides["user"] = globals.User.Value
var issueResp *jiradata.IssueCreateResponse
err = jiracli.EditLoop(&opts.CommonOptions, &input, &issueUpdate, func() error {
issueResp, err = jira.CreateIssue(o, globals.Endpoint.Value, &issueUpdate)
return err
})
if err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", issueResp.Key, globals.Endpoint.Value, issueResp.Key)
}
if opts.Browse.Value {
return CmdBrowse(globals, issueResp.Key)
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
func CmdTakeRegistry() *jiracli.CommandRegistryEntry {
opts := AssignOptions{}
return &jiracli.CommandRegistryEntry{
"Assign issue to yourself",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdAssignUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
opts.Assignee = globals.User.Value
return CmdAssign(o, globals, &opts)
},
}
}
func CmdTakeUsage(cmd *kingpin.CmdClause, opts *AssignOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Arg("ISSUE", "issue to assign").Required().StringVar(&opts.Issue)
return nil
}
+166
View File
@@ -0,0 +1,166 @@
package jiracmd
import (
"fmt"
"strings"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type TransitionOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Overrides map[string]string `yaml:"overrides,omitempty" json:"overrides,omitempty"`
Transition string `yaml:"transition,omitempty" json:"transition,omitempty"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
Resolution string `yaml:"resolution,omitempty" json:"resolution,omitempty"`
}
func CmdTransitionRegistry(transition string) *jiracli.CommandRegistryEntry {
opts := TransitionOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("transition"),
},
Overrides: map[string]string{},
}
help := "Transition issue to given state"
if transition != "" {
help = fmt.Sprintf("Transition issue to %s state", transition)
opts.SkipEditing = figtree.NewBoolOption(true)
}
return &jiracli.CommandRegistryEntry{
help,
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
if opts.Transition == "" {
opts.Transition = transition
}
return CmdTransitionUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdTransition(o, globals, &opts)
},
}
}
func CmdTransitionUsage(cmd *kingpin.CmdClause, opts *TransitionOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("noedit", "Disable opening the editor").SetValue(&opts.SkipEditing)
cmd.Flag("comment", "Comment message for issue").Short('m').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Overrides["comment"] = jiracli.FlagValue(ctx, "comment")
return nil
}).String()
cmd.Flag("override", "Set issue property").Short('o').StringMapVar(&opts.Overrides)
if opts.Transition == "" {
cmd.Arg("TRANSITION", "State to transition issue to").Required().StringVar(&opts.Transition)
}
cmd.Arg("ISSUE", "issue to transition").Required().StringVar(&opts.Issue)
return nil
}
func defaultResolution(transMeta *jiradata.Transition) string {
if resField, ok := transMeta.Fields["resolution"]; ok {
for _, allowedValueRaw := range resField.AllowedValues {
if allowedValue, ok := allowedValueRaw.(map[string]interface{}); ok {
if allowedValue["name"] == "Fixed" {
return "Fixed"
} else if allowedValue["name"] == "Done" {
return "Done"
}
}
}
}
return ""
}
// CmdTransition will move state of the given issue to the given transtion
func CmdTransition(o *oreo.Client, globals *jiracli.GlobalOptions, opts *TransitionOptions) error {
issueData, err := jira.GetIssue(o, globals.Endpoint.Value, opts.Issue, nil)
if err != nil {
return jiracli.CliError(err)
}
meta, err := jira.GetIssueTransitions(o, globals.Endpoint.Value, opts.Issue)
if err != nil {
return jiracli.CliError(err)
}
transMeta := meta.Transitions.Find(opts.Transition)
if transMeta == nil {
possible := []string{}
for _, trans := range meta.Transitions {
possible = append(possible, trans.Name)
}
if status, ok := issueData.Fields["status"].(map[string]interface{}); ok {
if name, ok := status["name"].(string); ok {
return jiracli.CliError(fmt.Errorf("Invalid Transition %q from %q, Available: %s", opts.Transition, name, strings.Join(possible, ", ")))
}
}
return jiracli.CliError(fmt.Errorf("No valid transition found matching %s", opts.Transition))
}
// need to default the Resolution, usually Fixed works but sometime need Done
if opts.Resolution == "" {
if resField, ok := transMeta.Fields["resolution"]; ok {
for _, allowedValueRaw := range resField.AllowedValues {
if allowedValue, ok := allowedValueRaw.(map[string]interface{}); ok {
if allowedValue["name"] == "Fixed" {
opts.Resolution = "Fixed"
} else if allowedValue["name"] == "Done" {
opts.Resolution = "Done"
}
}
}
}
}
opts.Overrides["resolution"] = opts.Resolution
type templateInput struct {
*jiradata.Issue `yaml:",inline"`
// Yes, Meta and Transition are redundant, but this is for backwards compatibility
// with old templates
Meta *jiradata.Transition `yaml:"meta,omitempty" json:"meta,omitemtpy"`
Transition *jiradata.Transition `yaml:"transition,omitempty" json:"transition,omitempty"`
Overrides map[string]string `yaml:"overrides,omitempty" json:"overrides,omitempty"`
}
if _, ok := transMeta.Fields["comment"]; !ok && opts.Overrides["comment"] != "" {
comment := jiradata.Comment{
Body: opts.Overrides["comment"],
}
if _, err := jira.IssueAddComment(o, globals.Endpoint.Value, opts.Issue, &comment); err != nil {
return err
}
}
issueUpdate := jiradata.IssueUpdate{}
input := templateInput{
Issue: issueData,
Meta: transMeta,
Transition: transMeta,
Overrides: opts.Overrides,
}
err = jiracli.EditLoop(&opts.CommonOptions, &input, &issueUpdate, func() error {
return jira.TransitionIssue(o, globals.Endpoint.Value, opts.Issue, &issueUpdate)
})
if err != nil {
return jiracli.CliError(err)
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", issueData.Key, globals.Endpoint.Value, issueData.Key)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type TransitionsOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
}
func CmdTransitionsRegistry(defaultTemplate string) *jiracli.CommandRegistryEntry {
opts := TransitionsOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption(defaultTemplate),
},
}
return &jiracli.CommandRegistryEntry{
"List valid issue transitions",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdTransitionsUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdTransitions(o, globals, &opts)
},
}
}
func CmdTransitionsUsage(cmd *kingpin.CmdClause, opts *TransitionsOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
jiracli.GJsonQueryUsage(cmd, &opts.CommonOptions)
cmd.Arg("ISSUE", "issue to list valid transitions").Required().StringVar(&opts.Issue)
return nil
}
// Transitions will get issue edit metadata and send to "editmeta" template
func CmdTransitions(o *oreo.Client, globals *jiracli.GlobalOptions, opts *TransitionsOptions) error {
editMeta, err := jira.GetIssueTransitions(o, globals.Endpoint.Value, opts.Issue)
if err != nil {
return err
}
if err := opts.PrintTemplate(editMeta); err != nil {
return err
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+29
View File
@@ -0,0 +1,29 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
func CmdUnassignRegistry() *jiracli.CommandRegistryEntry {
opts := AssignOptions{}
return &jiracli.CommandRegistryEntry{
"Unassign an issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdAssignUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdAssign(o, globals, &opts)
},
}
}
func CmdUnassignUsage(cmd *kingpin.CmdClause, opts *AssignOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Arg("ISSUE", "issue to unassign").Required().StringVar(&opts.Issue)
return nil
}
+63
View File
@@ -0,0 +1,63 @@
package jiracmd
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
func CmdUnexportTemplatesRegistry() *jiracli.CommandRegistryEntry {
opts := ExportTemplatesOptions{}
return &jiracli.CommandRegistryEntry{
"Remove unmodified exported templates",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
if opts.Dir != "" {
opts.Dir = fmt.Sprintf("%s/.jira.d/templates", jiracli.Homedir())
}
return CmdExportTemplatesUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdUnexportTemplates(globals, &opts)
},
}
}
// CmdUnexportTemplates will remove unmodified templates from export directory
func CmdUnexportTemplates(globals *jiracli.GlobalOptions, opts *ExportTemplatesOptions) error {
for name, template := range jiracli.AllTemplates {
if opts.Template != "" && opts.Template != name {
continue
}
templateFile := path.Join(opts.Dir, name)
if _, err := os.Stat(templateFile); err != nil {
log.Warning("Skipping %s, not found", templateFile)
continue
}
// open, read, compare
contents, err := ioutil.ReadFile(templateFile)
if err != nil {
return err
}
if bytes.Compare([]byte(template), contents) == 0 {
if !globals.Quiet.Value {
log.Notice("Removing %s, template identical to default", templateFile)
}
os.Remove(templateFile)
} else {
if !globals.Quiet.Value {
log.Notice("Skipping %s, found customizations to template", templateFile)
}
}
}
return nil
}
+60
View File
@@ -0,0 +1,60 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type ViewOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
jira.IssueOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
}
func CmdViewRegistry() *jiracli.CommandRegistryEntry {
opts := ViewOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("view"),
},
}
return &jiracli.CommandRegistryEntry{
"Prints issue details",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdViewUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdView(o, globals, &opts)
},
}
}
func CmdViewUsage(cmd *kingpin.CmdClause, opts *ViewOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
jiracli.GJsonQueryUsage(cmd, &opts.CommonOptions)
cmd.Flag("expand", "field to expand for the issue").StringsVar(&opts.Expand)
cmd.Flag("field", "field to return for the issue").StringsVar(&opts.Fields)
cmd.Flag("property", "property to return for issue").StringsVar(&opts.Properties)
cmd.Arg("ISSUE", "issue id to view").Required().StringVar(&opts.Issue)
return nil
}
// View will get issue data and send to "view" template
func CmdView(o *oreo.Client, globals *jiracli.GlobalOptions, opts *ViewOptions) error {
data, err := jira.GetIssue(o, globals.Endpoint.Value, opts.Issue, opts)
if err != nil {
return err
}
if err := opts.PrintTemplate(data); err != nil {
return err
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+73
View File
@@ -0,0 +1,73 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type VoteAction int
const (
VoteUP VoteAction = iota
VoteDown
)
type VoteOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
Action VoteAction `yaml:"-" json:"-"`
}
func CmdVoteRegistry() *jiracli.CommandRegistryEntry {
opts := VoteOptions{
CommonOptions: jiracli.CommonOptions{},
Action: VoteUP,
}
return &jiracli.CommandRegistryEntry{
"Vote up/down an issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdVoteUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdVote(o, globals, &opts)
},
}
}
func CmdVoteUsage(cmd *kingpin.CmdClause, opts *VoteOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Flag("down", "downvote the issue").Short('d').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Action = VoteDown
return nil
}).Bool()
cmd.Arg("ISSUE", "issue id to vote").StringVar(&opts.Issue)
return nil
}
// Vote will up/down vote an issue
func CmdVote(o *oreo.Client, globals *jiracli.GlobalOptions, opts *VoteOptions) error {
if opts.Action == VoteUP {
if err := jira.IssueAddVote(o, globals.Endpoint.Value, opts.Issue); err != nil {
return err
}
} else {
if err := jira.IssueRemoveVote(o, globals.Endpoint.Value, opts.Issue); err != nil {
return err
}
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+82
View File
@@ -0,0 +1,82 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type WatchAction int
const (
WatcherAdd WatchAction = iota
WatcherRemove
)
type WatchOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
Watcher string `yaml:"watcher,omitempty" json:"watcher,omitempty"`
Action WatchAction `yaml:"-" json:"-"`
}
func CmdWatchRegistry() *jiracli.CommandRegistryEntry {
opts := WatchOptions{
CommonOptions: jiracli.CommonOptions{},
Action: WatcherAdd,
}
return &jiracli.CommandRegistryEntry{
"Add/Remove watcher to issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdWatchUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdWatch(o, globals, &opts)
},
}
}
func CmdWatchUsage(cmd *kingpin.CmdClause, opts *WatchOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
cmd.Flag("remove", "remove watcher from issue").Short('r').PreAction(func(ctx *kingpin.ParseContext) error {
opts.Action = WatcherRemove
return nil
}).Bool()
cmd.Arg("ISSUE", "issue to add watcher").Required().StringVar(&opts.Issue)
cmd.Arg("WATCHER", "username of watcher to add to issue").StringVar(&opts.Watcher)
return nil
}
// CmdWatch will add the given watcher to the issue (or remove the watcher
// with the 'remove' flag)
func CmdWatch(o *oreo.Client, globals *jiracli.GlobalOptions, opts *WatchOptions) error {
if opts.Watcher == "" {
opts.Watcher = globals.User.Value
}
if opts.Action == WatcherAdd {
if err := jira.IssueAddWatcher(o, globals.Endpoint.Value, opts.Issue, opts.Watcher); err != nil {
return err
}
} else {
if err := jira.IssueRemoveWatcher(o, globals.Endpoint.Value, opts.Issue, opts.Watcher); err != nil {
return err
}
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+68
View File
@@ -0,0 +1,68 @@
package jiracmd
import (
"fmt"
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type WorklogAddOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
jiradata.Worklog `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
}
func CmdWorklogAddRegistry() *jiracli.CommandRegistryEntry {
opts := WorklogAddOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("worklog"),
},
}
return &jiracli.CommandRegistryEntry{
"Add a worklog to an issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdWorklogAddUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdWorklogAdd(o, globals, &opts)
},
}
}
func CmdWorklogAddUsage(cmd *kingpin.CmdClause, opts *WorklogAddOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.EditorUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
cmd.Flag("noedit", "Disable opening the editor").SetValue(&opts.SkipEditing)
cmd.Flag("comment", "Comment message for worklog").Short('m').StringVar(&opts.Comment)
cmd.Flag("time-spent", "Time spent working on issue").Short('T').StringVar(&opts.TimeSpent)
cmd.Flag("started", "Time you started work").Short('S').StringVar(&opts.Started)
cmd.Arg("ISSUE", "issue id to fetch worklogs").Required().StringVar(&opts.Issue)
return nil
}
// CmdWorklogAdd will attempt to add (action=add) a worklog to the given issue.
// It will spawn the editor (unless --noedit isused) and post edited YAML
// content as JSON to the worklog endpoint
func CmdWorklogAdd(o *oreo.Client, globals *jiracli.GlobalOptions, opts *WorklogAddOptions) error {
err := jiracli.EditLoop(&opts.CommonOptions, &opts.Worklog, &opts.Worklog, func() error {
_, err := jira.AddIssueWorklog(o, globals.Endpoint.Value, opts.Issue, opts)
return err
})
if err != nil {
return err
}
if !globals.Quiet.Value {
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+58
View File
@@ -0,0 +1,58 @@
package jiracmd
import (
"github.com/coryb/figtree"
"github.com/coryb/oreo"
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
kingpin "gopkg.in/alecthomas/kingpin.v2"
)
type WorklogListOptions struct {
jiracli.CommonOptions `yaml:",inline" json:",inline" figtree:",inline"`
Issue string `yaml:"issue,omitempty" json:"issue,omitempty"`
}
func CmdWorklogListRegistry() *jiracli.CommandRegistryEntry {
opts := WorklogListOptions{
CommonOptions: jiracli.CommonOptions{
Template: figtree.NewStringOption("worklogs"),
},
}
return &jiracli.CommandRegistryEntry{
"Prints the worklog data for given issue",
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
jiracli.LoadConfigs(cmd, fig, &opts)
return CmdWorklogListUsage(cmd, &opts)
},
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
return CmdWorklogList(o, globals, &opts)
},
}
}
func CmdWorklogListUsage(cmd *kingpin.CmdClause, opts *WorklogListOptions) error {
jiracli.BrowseUsage(cmd, &opts.CommonOptions)
jiracli.TemplateUsage(cmd, &opts.CommonOptions)
jiracli.GJsonQueryUsage(cmd, &opts.CommonOptions)
cmd.Arg("ISSUE", "issue id to fetch worklogs").Required().StringVar(&opts.Issue)
return nil
}
// // CmdWorklogList will get worklog data for given issue and sent to the "worklogs" template
func CmdWorklogList(o *oreo.Client, globals *jiracli.GlobalOptions, opts *WorklogListOptions) error {
data, err := jira.GetIssueWorklog(o, globals.Endpoint.Value, opts.Issue)
if err != nil {
return err
}
if err := opts.PrintTemplate(struct {
Worklogs *jiradata.Worklogs `json:"worklogs,omitempty" yaml:"worklogs,omitempty"`
}{data}); err != nil {
return err
}
if opts.Browse.Value {
return CmdBrowse(globals, opts.Issue)
}
return nil
}
+19
View File
@@ -0,0 +1,19 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/TransitionsMeta.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// AllowedValues defined from schema:
// {
// "title": "allowedValues",
// "type": "array",
// "items": {}
// }
type AllowedValues []interface{}
+179
View File
@@ -0,0 +1,179 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/ListofAttachment.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// Attachment defined from schema:
// {
// "title": "Attachment",
// "type": "object",
// "properties": {
// "author": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "title": "accountId",
// "type": "string"
// },
// "active": {
// "title": "active",
// "type": "boolean"
// },
// "applicationRoles": {
// "title": "Simple List Wrapper",
// "type": "object",
// "properties": {
// "items": {
// "type": "array",
// "items": {
// "title": "Group",
// "type": "object",
// "properties": {
// "name": {
// "type": "string"
// },
// "self": {
// "type": "string"
// }
// }
// }
// },
// "max-results": {
// "type": "integer"
// },
// "size": {
// "type": "integer"
// }
// }
// },
// "avatarUrls": {
// "title": "avatarUrls",
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "title": "displayName",
// "type": "string"
// },
// "emailAddress": {
// "title": "emailAddress",
// "type": "string"
// },
// "expand": {
// "title": "expand",
// "type": "string"
// },
// "groups": {
// "title": "Simple List Wrapper",
// "type": "object",
// "properties": {
// "items": {
// "type": "array",
// "items": {
// "title": "Group",
// "type": "object",
// "properties": {
// "name": {
// "type": "string"
// },
// "self": {
// "type": "string"
// }
// }
// }
// },
// "max-results": {
// "type": "integer"
// },
// "size": {
// "type": "integer"
// }
// }
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "locale": {
// "title": "locale",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// },
// "timeZone": {
// "title": "timeZone",
// "type": "string"
// }
// }
// },
// "content": {
// "title": "content",
// "type": "string"
// },
// "created": {
// "title": "created",
// "type": "string"
// },
// "filename": {
// "title": "filename",
// "type": "string"
// },
// "id": {
// "title": "id",
// "type": "integer"
// },
// "mimeType": {
// "title": "mimeType",
// "type": "string"
// },
// "properties": {
// "title": "properties",
// "type": "object",
// "patternProperties": {
// ".+": {}
// }
// },
// "self": {
// "title": "self",
// "type": "string"
// },
// "size": {
// "title": "size",
// "type": "integer"
// },
// "thumbnail": {
// "title": "thumbnail",
// "type": "string"
// }
// }
// }
type Attachment struct {
Author *User `json:"author,omitempty" yaml:"author,omitempty"`
Content string `json:"content,omitempty" yaml:"content,omitempty"`
Created string `json:"created,omitempty" yaml:"created,omitempty"`
Filename string `json:"filename,omitempty" yaml:"filename,omitempty"`
ID IntOrString `json:"id,omitempty" yaml:"id,omitempty"`
MimeType string `json:"mimeType,omitempty" yaml:"mimeType,omitempty"`
Properties map[string]interface{} `json:"properties,omitempty" yaml:"properties,omitempty"`
Self string `json:"self,omitempty" yaml:"self,omitempty"`
Size int `json:"size,omitempty" yaml:"size,omitempty"`
Thumbnail string `json:"thumbnail,omitempty" yaml:"thumbnail,omitempty"`
}
+32
View File
@@ -0,0 +1,32 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/AuthParams.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// AuthParams defined from schema:
// {
// "title": "Auth Params",
// "id": "https://docs.atlassian.com/jira/REST/schema/auth-params#",
// "type": "object",
// "properties": {
// "password": {
// "title": "password",
// "type": "string"
// },
// "username": {
// "title": "username",
// "type": "string"
// }
// }
// }
type AuthParams struct {
Password string `json:"password,omitempty" yaml:"password,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"`
}
+60
View File
@@ -0,0 +1,60 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/AuthSuccess.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// AuthSuccess defined from schema:
// {
// "title": "Auth Success",
// "id": "https://docs.atlassian.com/jira/REST/schema/auth-success#",
// "type": "object",
// "properties": {
// "loginInfo": {
// "title": "Login Info",
// "type": "object",
// "properties": {
// "failedLoginCount": {
// "title": "failedLoginCount",
// "type": "integer"
// },
// "lastFailedLoginTime": {
// "title": "lastFailedLoginTime",
// "type": "string"
// },
// "loginCount": {
// "title": "loginCount",
// "type": "integer"
// },
// "previousLoginTime": {
// "title": "previousLoginTime",
// "type": "string"
// }
// }
// },
// "session": {
// "title": "Session Info",
// "type": "object",
// "properties": {
// "name": {
// "title": "name",
// "type": "string"
// },
// "value": {
// "title": "value",
// "type": "string"
// }
// }
// }
// }
// }
type AuthSuccess struct {
LoginInfo *LoginInfo `json:"loginInfo,omitempty" yaml:"loginInfo,omitempty"`
Session *SessionInfo `json:"session,omitempty" yaml:"session,omitempty"`
}
+234
View File
@@ -0,0 +1,234 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/SearchResults.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// ChangeHistory defined from schema:
// {
// "title": "Change History",
// "type": "object",
// "properties": {
// "author": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "title": "accountId",
// "type": "string"
// },
// "active": {
// "title": "active",
// "type": "boolean"
// },
// "avatarUrls": {
// "title": "avatarUrls",
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "title": "displayName",
// "type": "string"
// },
// "emailAddress": {
// "title": "emailAddress",
// "type": "string"
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// },
// "timeZone": {
// "title": "timeZone",
// "type": "string"
// }
// }
// },
// "created": {
// "title": "created",
// "type": "string"
// },
// "historyMetadata": {
// "title": "History Metadata",
// "type": "object",
// "properties": {
// "activityDescription": {
// "title": "activityDescription",
// "type": "string"
// },
// "activityDescriptionKey": {
// "title": "activityDescriptionKey",
// "type": "string"
// },
// "actor": {
// "title": "History Metadata Participant",
// "type": "object",
// "properties": {
// "avatarUrl": {
// "type": "string"
// },
// "displayName": {
// "type": "string"
// },
// "displayNameKey": {
// "type": "string"
// },
// "id": {
// "type": "string"
// },
// "type": {
// "type": "string"
// },
// "url": {
// "type": "string"
// }
// }
// },
// "cause": {
// "title": "History Metadata Participant",
// "type": "object",
// "properties": {
// "avatarUrl": {
// "type": "string"
// },
// "displayName": {
// "type": "string"
// },
// "displayNameKey": {
// "type": "string"
// },
// "id": {
// "type": "string"
// },
// "type": {
// "type": "string"
// },
// "url": {
// "type": "string"
// }
// }
// },
// "description": {
// "title": "description",
// "type": "string"
// },
// "descriptionKey": {
// "title": "descriptionKey",
// "type": "string"
// },
// "emailDescription": {
// "title": "emailDescription",
// "type": "string"
// },
// "emailDescriptionKey": {
// "title": "emailDescriptionKey",
// "type": "string"
// },
// "extraData": {
// "title": "extraData",
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "generator": {
// "title": "History Metadata Participant",
// "type": "object",
// "properties": {
// "avatarUrl": {
// "type": "string"
// },
// "displayName": {
// "type": "string"
// },
// "displayNameKey": {
// "type": "string"
// },
// "id": {
// "type": "string"
// },
// "type": {
// "type": "string"
// },
// "url": {
// "type": "string"
// }
// }
// },
// "type": {
// "title": "type",
// "type": "string"
// }
// }
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "items": {
// "title": "items",
// "type": "array",
// "items": {
// "title": "Change Item",
// "type": "object",
// "properties": {
// "field": {
// "title": "field",
// "type": "string"
// },
// "fieldId": {
// "title": "fieldId",
// "type": "string"
// },
// "fieldtype": {
// "title": "fieldtype",
// "type": "string"
// },
// "from": {
// "title": "from",
// "type": "string"
// },
// "fromString": {
// "title": "fromString",
// "type": "string"
// },
// "to": {
// "title": "to",
// "type": "string"
// },
// "toString": {
// "title": "toString",
// "type": "string"
// }
// }
// }
// }
// }
// }
type ChangeHistory struct {
Author *User `json:"author,omitempty" yaml:"author,omitempty"`
Created string `json:"created,omitempty" yaml:"created,omitempty"`
HistoryMetadata *HistoryMetadata `json:"historyMetadata,omitempty" yaml:"historyMetadata,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Items Items `json:"items,omitempty" yaml:"items,omitempty"`
}
+56
View File
@@ -0,0 +1,56 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/SearchResults.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// ChangeItem defined from schema:
// {
// "title": "Change Item",
// "type": "object",
// "properties": {
// "field": {
// "title": "field",
// "type": "string"
// },
// "fieldId": {
// "title": "fieldId",
// "type": "string"
// },
// "fieldtype": {
// "title": "fieldtype",
// "type": "string"
// },
// "from": {
// "title": "from",
// "type": "string"
// },
// "fromString": {
// "title": "fromString",
// "type": "string"
// },
// "to": {
// "title": "to",
// "type": "string"
// },
// "toString": {
// "title": "toString",
// "type": "string"
// }
// }
// }
type ChangeItem struct {
Field string `json:"field,omitempty" yaml:"field,omitempty"`
FieldID string `json:"fieldId,omitempty" yaml:"fieldId,omitempty"`
FieldType string `json:"fieldtype,omitempty" yaml:"fieldtype,omitempty"`
From string `json:"from,omitempty" yaml:"from,omitempty"`
FromString string `json:"fromString,omitempty" yaml:"fromString,omitempty"`
To string `json:"to,omitempty" yaml:"to,omitempty"`
ToString string `json:"toString,omitempty" yaml:"toString,omitempty"`
}
+255
View File
@@ -0,0 +1,255 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/SearchResults.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// Changelog defined from schema:
// {
// "title": "Changelog",
// "type": "object",
// "properties": {
// "histories": {
// "title": "histories",
// "type": "array",
// "items": {
// "title": "Change History",
// "type": "object",
// "properties": {
// "author": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "title": "accountId",
// "type": "string"
// },
// "active": {
// "title": "active",
// "type": "boolean"
// },
// "avatarUrls": {
// "title": "avatarUrls",
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "title": "displayName",
// "type": "string"
// },
// "emailAddress": {
// "title": "emailAddress",
// "type": "string"
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// },
// "timeZone": {
// "title": "timeZone",
// "type": "string"
// }
// }
// },
// "created": {
// "title": "created",
// "type": "string"
// },
// "historyMetadata": {
// "title": "History Metadata",
// "type": "object",
// "properties": {
// "activityDescription": {
// "title": "activityDescription",
// "type": "string"
// },
// "activityDescriptionKey": {
// "title": "activityDescriptionKey",
// "type": "string"
// },
// "actor": {
// "title": "History Metadata Participant",
// "type": "object",
// "properties": {
// "avatarUrl": {
// "type": "string"
// },
// "displayName": {
// "type": "string"
// },
// "displayNameKey": {
// "type": "string"
// },
// "id": {
// "type": "string"
// },
// "type": {
// "type": "string"
// },
// "url": {
// "type": "string"
// }
// }
// },
// "cause": {
// "title": "History Metadata Participant",
// "type": "object",
// "properties": {
// "avatarUrl": {
// "type": "string"
// },
// "displayName": {
// "type": "string"
// },
// "displayNameKey": {
// "type": "string"
// },
// "id": {
// "type": "string"
// },
// "type": {
// "type": "string"
// },
// "url": {
// "type": "string"
// }
// }
// },
// "description": {
// "title": "description",
// "type": "string"
// },
// "descriptionKey": {
// "title": "descriptionKey",
// "type": "string"
// },
// "emailDescription": {
// "title": "emailDescription",
// "type": "string"
// },
// "emailDescriptionKey": {
// "title": "emailDescriptionKey",
// "type": "string"
// },
// "extraData": {
// "title": "extraData",
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "generator": {
// "title": "History Metadata Participant",
// "type": "object",
// "properties": {
// "avatarUrl": {
// "type": "string"
// },
// "displayName": {
// "type": "string"
// },
// "displayNameKey": {
// "type": "string"
// },
// "id": {
// "type": "string"
// },
// "type": {
// "type": "string"
// },
// "url": {
// "type": "string"
// }
// }
// },
// "type": {
// "title": "type",
// "type": "string"
// }
// }
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "items": {
// "title": "items",
// "type": "array",
// "items": {
// "title": "Change Item",
// "type": "object",
// "properties": {
// "field": {
// "title": "field",
// "type": "string"
// },
// "fieldId": {
// "title": "fieldId",
// "type": "string"
// },
// "fieldtype": {
// "title": "fieldtype",
// "type": "string"
// },
// "from": {
// "title": "from",
// "type": "string"
// },
// "fromString": {
// "title": "fromString",
// "type": "string"
// },
// "to": {
// "title": "to",
// "type": "string"
// },
// "toString": {
// "title": "toString",
// "type": "string"
// }
// }
// }
// }
// }
// }
// },
// "maxResults": {
// "title": "maxResults",
// "type": "integer"
// },
// "startAt": {
// "title": "startAt",
// "type": "integer"
// },
// "total": {
// "title": "total",
// "type": "integer"
// }
// }
// }
type Changelog struct {
Histories Histories `json:"histories,omitempty" yaml:"histories,omitempty"`
MaxResults int `json:"maxResults,omitempty" yaml:"maxResults,omitempty"`
StartAt int `json:"startAt,omitempty" yaml:"startAt,omitempty"`
Total int `json:"total,omitempty" yaml:"total,omitempty"`
}
+21
View File
@@ -0,0 +1,21 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/Field.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// ClauseNames defined from schema:
// {
// "title": "clauseNames",
// "type": "array",
// "items": {
// "type": "string"
// }
// }
type ClauseNames []string
+151
View File
@@ -0,0 +1,151 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/LinkIssueRequest.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// Comment defined from schema:
// {
// "title": "Comment",
// "type": "object",
// "properties": {
// "author": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "type": "string"
// },
// "active": {
// "type": "boolean"
// },
// "avatarUrls": {
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "type": "string"
// },
// "emailAddress": {
// "type": "string"
// },
// "key": {
// "type": "string"
// },
// "name": {
// "type": "string"
// },
// "timeZone": {
// "type": "string"
// }
// }
// },
// "body": {
// "title": "body",
// "type": "string"
// },
// "created": {
// "title": "created",
// "type": "string"
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "properties": {
// "title": "properties",
// "type": "array",
// "items": {
// "title": "Entity Property",
// "type": "object",
// "properties": {
// "key": {
// "title": "key",
// "type": "string"
// },
// "value": {
// "title": "value"
// }
// }
// }
// },
// "renderedBody": {
// "title": "renderedBody",
// "type": "string"
// },
// "updateAuthor": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "type": "string"
// },
// "active": {
// "type": "boolean"
// },
// "avatarUrls": {
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "type": "string"
// },
// "emailAddress": {
// "type": "string"
// },
// "key": {
// "type": "string"
// },
// "name": {
// "type": "string"
// },
// "timeZone": {
// "type": "string"
// }
// }
// },
// "updated": {
// "title": "updated",
// "type": "string"
// },
// "visibility": {
// "title": "Visibility",
// "type": "object",
// "properties": {
// "type": {
// "title": "type",
// "type": "string"
// },
// "value": {
// "title": "value",
// "type": "string"
// }
// }
// }
// }
// }
type Comment struct {
Author *User `json:"author,omitempty" yaml:"author,omitempty"`
Body string `json:"body,omitempty" yaml:"body,omitempty"`
Created string `json:"created,omitempty" yaml:"created,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Properties Properties `json:"properties,omitempty" yaml:"properties,omitempty"`
RenderedBody string `json:"renderedBody,omitempty" yaml:"renderedBody,omitempty"`
UpdateAuthor *User `json:"updateAuthor,omitempty" yaml:"updateAuthor,omitempty"`
Updated string `json:"updated,omitempty" yaml:"updated,omitempty"`
Visibility *Visibility `json:"visibility,omitempty" yaml:"visibility,omitempty"`
}
+224
View File
@@ -0,0 +1,224 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/Project.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// Component defined from schema:
// {
// "title": "Component",
// "type": "object",
// "properties": {
// "assignee": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "type": "string"
// },
// "active": {
// "type": "boolean"
// },
// "applicationRoles": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "avatarUrls": {
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "type": "string"
// },
// "emailAddress": {
// "type": "string"
// },
// "expand": {
// "type": "string"
// },
// "groups": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "key": {
// "type": "string"
// },
// "locale": {
// "type": "string"
// },
// "name": {
// "type": "string"
// },
// "self": {
// "type": "string"
// },
// "timeZone": {
// "type": "string"
// }
// }
// },
// "assigneeType": {
// "title": "assigneeType",
// "type": "string"
// },
// "description": {
// "title": "description",
// "type": "string"
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "isAssigneeTypeValid": {
// "title": "isAssigneeTypeValid",
// "type": "boolean"
// },
// "lead": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "type": "string"
// },
// "active": {
// "type": "boolean"
// },
// "applicationRoles": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "avatarUrls": {
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "type": "string"
// },
// "emailAddress": {
// "type": "string"
// },
// "expand": {
// "type": "string"
// },
// "groups": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "key": {
// "type": "string"
// },
// "locale": {
// "type": "string"
// },
// "name": {
// "type": "string"
// },
// "self": {
// "type": "string"
// },
// "timeZone": {
// "type": "string"
// }
// }
// },
// "leadUserName": {
// "title": "leadUserName",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "project": {
// "title": "project",
// "type": "string"
// },
// "projectId": {
// "title": "projectId",
// "type": "integer"
// },
// "realAssignee": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "type": "string"
// },
// "active": {
// "type": "boolean"
// },
// "applicationRoles": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "avatarUrls": {
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "type": "string"
// },
// "emailAddress": {
// "type": "string"
// },
// "expand": {
// "type": "string"
// },
// "groups": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "key": {
// "type": "string"
// },
// "locale": {
// "type": "string"
// },
// "name": {
// "type": "string"
// },
// "self": {
// "type": "string"
// },
// "timeZone": {
// "type": "string"
// }
// }
// },
// "realAssigneeType": {
// "title": "realAssigneeType",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// }
// }
// }
type Component struct {
Assignee *User `json:"assignee,omitempty" yaml:"assignee,omitempty"`
AssigneeType string `json:"assigneeType,omitempty" yaml:"assigneeType,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
IsAssigneeTypeValid bool `json:"isAssigneeTypeValid,omitempty" yaml:"isAssigneeTypeValid,omitempty"`
Lead *User `json:"lead,omitempty" yaml:"lead,omitempty"`
LeadUserName string `json:"leadUserName,omitempty" yaml:"leadUserName,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Project string `json:"project,omitempty" yaml:"project,omitempty"`
ProjectID int `json:"projectId,omitempty" yaml:"projectId,omitempty"`
RealAssignee *User `json:"realAssignee,omitempty" yaml:"realAssignee,omitempty"`
RealAssigneeType string `json:"realAssigneeType,omitempty" yaml:"realAssigneeType,omitempty"`
Self string `json:"self,omitempty" yaml:"self,omitempty"`
}
+214
View File
@@ -0,0 +1,214 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/Project.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// Components defined from schema:
// {
// "title": "components",
// "type": "array",
// "items": {
// "title": "Component",
// "type": "object",
// "properties": {
// "assignee": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "type": "string"
// },
// "active": {
// "type": "boolean"
// },
// "applicationRoles": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "avatarUrls": {
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "type": "string"
// },
// "emailAddress": {
// "type": "string"
// },
// "expand": {
// "type": "string"
// },
// "groups": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "key": {
// "type": "string"
// },
// "locale": {
// "type": "string"
// },
// "name": {
// "type": "string"
// },
// "self": {
// "type": "string"
// },
// "timeZone": {
// "type": "string"
// }
// }
// },
// "assigneeType": {
// "title": "assigneeType",
// "type": "string"
// },
// "description": {
// "title": "description",
// "type": "string"
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "isAssigneeTypeValid": {
// "title": "isAssigneeTypeValid",
// "type": "boolean"
// },
// "lead": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "type": "string"
// },
// "active": {
// "type": "boolean"
// },
// "applicationRoles": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "avatarUrls": {
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "type": "string"
// },
// "emailAddress": {
// "type": "string"
// },
// "expand": {
// "type": "string"
// },
// "groups": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "key": {
// "type": "string"
// },
// "locale": {
// "type": "string"
// },
// "name": {
// "type": "string"
// },
// "self": {
// "type": "string"
// },
// "timeZone": {
// "type": "string"
// }
// }
// },
// "leadUserName": {
// "title": "leadUserName",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "project": {
// "title": "project",
// "type": "string"
// },
// "projectId": {
// "title": "projectId",
// "type": "integer"
// },
// "realAssignee": {
// "title": "User",
// "type": "object",
// "properties": {
// "accountId": {
// "type": "string"
// },
// "active": {
// "type": "boolean"
// },
// "applicationRoles": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "avatarUrls": {
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "displayName": {
// "type": "string"
// },
// "emailAddress": {
// "type": "string"
// },
// "expand": {
// "type": "string"
// },
// "groups": {
// "$ref": "#/definitions/simple-list-wrapper"
// },
// "key": {
// "type": "string"
// },
// "locale": {
// "type": "string"
// },
// "name": {
// "type": "string"
// },
// "self": {
// "type": "string"
// },
// "timeZone": {
// "type": "string"
// }
// }
// },
// "realAssigneeType": {
// "title": "realAssigneeType",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// }
// }
// }
// }
type Components []*Component
+182
View File
@@ -0,0 +1,182 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/CreateMeta.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// CreateMeta defined from schema:
// {
// "title": "Create Meta",
// "id": "https://docs.atlassian.com/jira/REST/schema/create-meta#",
// "type": "object",
// "properties": {
// "expand": {
// "title": "expand",
// "type": "string"
// },
// "projects": {
// "title": "projects",
// "type": "array",
// "items": {
// "title": "Create Meta Project",
// "type": "object",
// "properties": {
// "avatarUrls": {
// "title": "avatarUrls",
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "expand": {
// "title": "expand",
// "type": "string"
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "issuetypes": {
// "title": "issuetypes",
// "type": "array",
// "items": {
// "title": "Create Meta Issue Type",
// "type": "object",
// "properties": {
// "avatarId": {
// "title": "avatarId",
// "type": "integer"
// },
// "description": {
// "title": "description",
// "type": "string"
// },
// "expand": {
// "title": "expand",
// "type": "string"
// },
// "fields": {
// "title": "fields",
// "type": "object",
// "patternProperties": {
// ".+": {
// "title": "Field Meta",
// "type": "object",
// "properties": {
// "allowedValues": {
// "title": "allowedValues",
// "type": "array",
// "items": {}
// },
// "autoCompleteUrl": {
// "title": "autoCompleteUrl",
// "type": "string"
// },
// "defaultValue": {
// "title": "defaultValue"
// },
// "hasDefaultValue": {
// "title": "hasDefaultValue",
// "type": "boolean"
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "operations": {
// "title": "operations",
// "type": "array",
// "items": {
// "type": "string"
// }
// },
// "required": {
// "title": "required",
// "type": "boolean"
// },
// "schema": {
// "title": "Json Type",
// "type": "object",
// "properties": {
// "custom": {
// "title": "custom",
// "type": "string"
// },
// "customId": {
// "title": "customId",
// "type": "integer"
// },
// "items": {
// "title": "items",
// "type": "string"
// },
// "system": {
// "title": "system",
// "type": "string"
// },
// "type": {
// "title": "type",
// "type": "string"
// }
// }
// }
// }
// }
// }
// },
// "iconUrl": {
// "title": "iconUrl",
// "type": "string"
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// },
// "subtask": {
// "title": "subtask",
// "type": "boolean"
// }
// }
// }
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// }
// }
// }
// }
// }
// }
type CreateMeta struct {
Expand string `json:"expand,omitempty" yaml:"expand,omitempty"`
Projects Projects `json:"projects,omitempty" yaml:"projects,omitempty"`
}
+135
View File
@@ -0,0 +1,135 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/CreateMeta.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// CreateMetaIssueType defined from schema:
// {
// "title": "Create Meta Issue Type",
// "type": "object",
// "properties": {
// "avatarId": {
// "title": "avatarId",
// "type": "integer"
// },
// "description": {
// "title": "description",
// "type": "string"
// },
// "expand": {
// "title": "expand",
// "type": "string"
// },
// "fields": {
// "title": "fields",
// "type": "object",
// "patternProperties": {
// ".+": {
// "title": "Field Meta",
// "type": "object",
// "properties": {
// "allowedValues": {
// "title": "allowedValues",
// "type": "array",
// "items": {}
// },
// "autoCompleteUrl": {
// "title": "autoCompleteUrl",
// "type": "string"
// },
// "defaultValue": {
// "title": "defaultValue"
// },
// "hasDefaultValue": {
// "title": "hasDefaultValue",
// "type": "boolean"
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "operations": {
// "title": "operations",
// "type": "array",
// "items": {
// "type": "string"
// }
// },
// "required": {
// "title": "required",
// "type": "boolean"
// },
// "schema": {
// "title": "Json Type",
// "type": "object",
// "properties": {
// "custom": {
// "title": "custom",
// "type": "string"
// },
// "customId": {
// "title": "customId",
// "type": "integer"
// },
// "items": {
// "title": "items",
// "type": "string"
// },
// "system": {
// "title": "system",
// "type": "string"
// },
// "type": {
// "title": "type",
// "type": "string"
// }
// }
// }
// }
// }
// }
// },
// "iconUrl": {
// "title": "iconUrl",
// "type": "string"
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// },
// "subtask": {
// "title": "subtask",
// "type": "boolean"
// }
// }
// }
type CreateMetaIssueType struct {
AvatarID int `json:"avatarId,omitempty" yaml:"avatarId,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Expand string `json:"expand,omitempty" yaml:"expand,omitempty"`
Fields FieldMetaMap `json:"fields,omitempty" yaml:"fields,omitempty"`
IconURL string `json:"iconUrl,omitempty" yaml:"iconUrl,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Self string `json:"self,omitempty" yaml:"self,omitempty"`
Subtask bool `json:"subtask,omitempty" yaml:"subtask,omitempty"`
}
+172
View File
@@ -0,0 +1,172 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/CreateMeta.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// CreateMetaProject defined from schema:
// {
// "title": "Create Meta Project",
// "type": "object",
// "properties": {
// "avatarUrls": {
// "title": "avatarUrls",
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "expand": {
// "title": "expand",
// "type": "string"
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "issuetypes": {
// "title": "issuetypes",
// "type": "array",
// "items": {
// "title": "Create Meta Issue Type",
// "type": "object",
// "properties": {
// "avatarId": {
// "title": "avatarId",
// "type": "integer"
// },
// "description": {
// "title": "description",
// "type": "string"
// },
// "expand": {
// "title": "expand",
// "type": "string"
// },
// "fields": {
// "title": "fields",
// "type": "object",
// "patternProperties": {
// ".+": {
// "title": "Field Meta",
// "type": "object",
// "properties": {
// "allowedValues": {
// "title": "allowedValues",
// "type": "array",
// "items": {}
// },
// "autoCompleteUrl": {
// "title": "autoCompleteUrl",
// "type": "string"
// },
// "defaultValue": {
// "title": "defaultValue"
// },
// "hasDefaultValue": {
// "title": "hasDefaultValue",
// "type": "boolean"
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "operations": {
// "title": "operations",
// "type": "array",
// "items": {
// "type": "string"
// }
// },
// "required": {
// "title": "required",
// "type": "boolean"
// },
// "schema": {
// "title": "Json Type",
// "type": "object",
// "properties": {
// "custom": {
// "title": "custom",
// "type": "string"
// },
// "customId": {
// "title": "customId",
// "type": "integer"
// },
// "items": {
// "title": "items",
// "type": "string"
// },
// "system": {
// "title": "system",
// "type": "string"
// },
// "type": {
// "title": "type",
// "type": "string"
// }
// }
// }
// }
// }
// }
// },
// "iconUrl": {
// "title": "iconUrl",
// "type": "string"
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// },
// "subtask": {
// "title": "subtask",
// "type": "boolean"
// }
// }
// }
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// }
// }
// }
type CreateMetaProject struct {
AvatarUrls map[string]string `json:"avatarUrls,omitempty" yaml:"avatarUrls,omitempty"`
Expand string `json:"expand,omitempty" yaml:"expand,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
IssueTypes IssueTypes `json:"issuetypes,omitempty" yaml:"issuetypes,omitempty"`
Key string `json:"key,omitempty" yaml:"key,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Self string `json:"self,omitempty" yaml:"self,omitempty"`
}
+55
View File
@@ -0,0 +1,55 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/CurrentUser.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// CurrentUser defined from schema:
// {
// "title": "Current User",
// "id": "https://docs.atlassian.com/jira/REST/schema/current-user#",
// "type": "object",
// "properties": {
// "loginInfo": {
// "title": "Login Info",
// "type": "object",
// "properties": {
// "failedLoginCount": {
// "title": "failedLoginCount",
// "type": "integer"
// },
// "lastFailedLoginTime": {
// "title": "lastFailedLoginTime",
// "type": "string"
// },
// "loginCount": {
// "title": "loginCount",
// "type": "integer"
// },
// "previousLoginTime": {
// "title": "previousLoginTime",
// "type": "string"
// }
// }
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "self": {
// "title": "self",
// "type": "string"
// }
// }
// }
type CurrentUser struct {
LoginInfo *LoginInfo `json:"loginInfo,omitempty" yaml:"loginInfo,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Self string `json:"self,omitempty" yaml:"self,omitempty"`
}
+63
View File
@@ -0,0 +1,63 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/SearchResults.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// EditMeta defined from schema:
// {
// "title": "Edit Meta",
// "type": "object",
// "properties": {
// "fields": {
// "title": "fields",
// "type": "object",
// "patternProperties": {
// ".+": {
// "title": "Field Meta",
// "type": "object",
// "properties": {
// "allowedValues": {
// "type": "array",
// "items": {}
// },
// "autoCompleteUrl": {
// "type": "string"
// },
// "defaultValue": {},
// "hasDefaultValue": {
// "type": "boolean"
// },
// "key": {
// "type": "string"
// },
// "name": {
// "type": "string"
// },
// "operations": {
// "type": "array",
// "items": {
// "type": "string"
// }
// },
// "required": {
// "type": "boolean"
// },
// "schema": {
// "$ref": "#/definitions/json-type"
// }
// }
// }
// }
// }
// }
// }
type EditMeta struct {
Fields FieldMetaMap `json:"fields,omitempty" yaml:"fields,omitempty"`
}
+30
View File
@@ -0,0 +1,30 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/WorklogWithPagination.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// EntityProperty defined from schema:
// {
// "title": "Entity Property",
// "type": "object",
// "properties": {
// "key": {
// "title": "key",
// "type": "string"
// },
// "value": {
// "title": "value"
// }
// }
// }
type EntityProperty struct {
Key string `json:"key,omitempty" yaml:"key,omitempty"`
Value interface{} `json:"value,omitempty" yaml:"value,omitempty"`
}
+5
View File
@@ -0,0 +1,5 @@
package jiradata
type EpicIssues struct {
Issues []string `json:"issues,omitempty" yaml:"issues,omitempty"`
}
+45
View File
@@ -0,0 +1,45 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/ErrorCollection.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// ErrorCollection defined from schema:
// {
// "title": "Error Collection",
// "id": "https://docs.atlassian.com/jira/REST/schema/error-collection#",
// "type": "object",
// "properties": {
// "errorMessages": {
// "title": "errorMessages",
// "type": "array",
// "items": {
// "type": "string"
// }
// },
// "errors": {
// "title": "errors",
// "type": "object",
// "patternProperties": {
// ".+": {
// "type": "string"
// }
// }
// },
// "status": {
// "title": "status",
// "type": "integer"
// }
// }
// }
type ErrorCollection struct {
ErrorMessages ErrorMessages `json:"errorMessages,omitempty" yaml:"errorMessages,omitempty"`
Errors map[string]string `json:"errors,omitempty" yaml:"errors,omitempty"`
Status int `json:"status,omitempty" yaml:"status,omitempty"`
}
+18
View File
@@ -0,0 +1,18 @@
package jiradata
import "strings"
// Error is needed to make ErrorCollection implement the error interface
func (e ErrorCollection) Error() string {
if len(e.ErrorMessages) > 0 {
return strings.Join(e.ErrorMessages, ". ")
}
out := ""
for k, v := range e.Errors {
if len(out) > 0 {
out += ". "
}
out += k + ": " + v
}
return out
}
+21
View File
@@ -0,0 +1,21 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/ErrorCollection.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// ErrorMessages defined from schema:
// {
// "title": "errorMessages",
// "type": "array",
// "items": {
// "type": "string"
// }
// }
type ErrorMessages []string
+92
View File
@@ -0,0 +1,92 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/Field.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// Field defined from schema:
// {
// "title": "Field",
// "id": "https://docs.atlassian.com/jira/REST/schema/field#",
// "type": "object",
// "properties": {
// "clauseNames": {
// "title": "clauseNames",
// "type": "array",
// "items": {
// "type": "string"
// }
// },
// "custom": {
// "title": "custom",
// "type": "boolean"
// },
// "id": {
// "title": "id",
// "type": "string"
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "navigable": {
// "title": "navigable",
// "type": "boolean"
// },
// "orderable": {
// "title": "orderable",
// "type": "boolean"
// },
// "schema": {
// "title": "Json Type",
// "type": "object",
// "properties": {
// "custom": {
// "title": "custom",
// "type": "string"
// },
// "customId": {
// "title": "customId",
// "type": "integer"
// },
// "items": {
// "title": "items",
// "type": "string"
// },
// "system": {
// "title": "system",
// "type": "string"
// },
// "type": {
// "title": "type",
// "type": "string"
// }
// }
// },
// "searchable": {
// "title": "searchable",
// "type": "boolean"
// }
// }
// }
type Field struct {
ClauseNames ClauseNames `json:"clauseNames,omitempty" yaml:"clauseNames,omitempty"`
Custom bool `json:"custom,omitempty" yaml:"custom,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Key string `json:"key,omitempty" yaml:"key,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Navigable bool `json:"navigable,omitempty" yaml:"navigable,omitempty"`
Orderable bool `json:"orderable,omitempty" yaml:"orderable,omitempty"`
Schema *JSONType `json:"schema,omitempty" yaml:"schema,omitempty"`
Searchable bool `json:"searchable,omitempty" yaml:"searchable,omitempty"`
}
+91
View File
@@ -0,0 +1,91 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/TransitionsMeta.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// FieldMeta defined from schema:
// {
// "title": "Field Meta",
// "type": "object",
// "properties": {
// "allowedValues": {
// "title": "allowedValues",
// "type": "array",
// "items": {}
// },
// "autoCompleteUrl": {
// "title": "autoCompleteUrl",
// "type": "string"
// },
// "defaultValue": {
// "title": "defaultValue"
// },
// "hasDefaultValue": {
// "title": "hasDefaultValue",
// "type": "boolean"
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "operations": {
// "title": "operations",
// "type": "array",
// "items": {
// "type": "string"
// }
// },
// "required": {
// "title": "required",
// "type": "boolean"
// },
// "schema": {
// "title": "Json Type",
// "type": "object",
// "properties": {
// "custom": {
// "title": "custom",
// "type": "string"
// },
// "customId": {
// "title": "customId",
// "type": "integer"
// },
// "items": {
// "title": "items",
// "type": "string"
// },
// "system": {
// "title": "system",
// "type": "string"
// },
// "type": {
// "title": "type",
// "type": "string"
// }
// }
// }
// }
// }
type FieldMeta struct {
AllowedValues AllowedValues `json:"allowedValues,omitempty" yaml:"allowedValues,omitempty"`
AutoCompleteURL string `json:"autoCompleteUrl,omitempty" yaml:"autoCompleteUrl,omitempty"`
DefaultValue interface{} `json:"defaultValue,omitempty" yaml:"defaultValue,omitempty"`
HasDefaultValue bool `json:"hasDefaultValue,omitempty" yaml:"hasDefaultValue,omitempty"`
Key string `json:"key,omitempty" yaml:"key,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Operations Operations `json:"operations,omitempty" yaml:"operations,omitempty"`
Required bool `json:"required,omitempty" yaml:"required,omitempty"`
Schema *JSONType `json:"schema,omitempty" yaml:"schema,omitempty"`
}
+87
View File
@@ -0,0 +1,87 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/TransitionsMeta.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// FieldMetaMap defined from schema:
// {
// "title": "fields",
// "type": "object",
// "patternProperties": {
// ".+": {
// "title": "Field Meta",
// "type": "object",
// "properties": {
// "allowedValues": {
// "title": "allowedValues",
// "type": "array",
// "items": {}
// },
// "autoCompleteUrl": {
// "title": "autoCompleteUrl",
// "type": "string"
// },
// "defaultValue": {
// "title": "defaultValue"
// },
// "hasDefaultValue": {
// "title": "hasDefaultValue",
// "type": "boolean"
// },
// "key": {
// "title": "key",
// "type": "string"
// },
// "name": {
// "title": "name",
// "type": "string"
// },
// "operations": {
// "title": "operations",
// "type": "array",
// "items": {
// "type": "string"
// }
// },
// "required": {
// "title": "required",
// "type": "boolean"
// },
// "schema": {
// "title": "Json Type",
// "type": "object",
// "properties": {
// "custom": {
// "title": "custom",
// "type": "string"
// },
// "customId": {
// "title": "customId",
// "type": "integer"
// },
// "items": {
// "title": "items",
// "type": "string"
// },
// "system": {
// "title": "system",
// "type": "string"
// },
// "type": {
// "title": "type",
// "type": "string"
// }
// }
// }
// }
// }
// }
// }
type FieldMetaMap map[string]*FieldMeta
+18
View File
@@ -0,0 +1,18 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/IssueUpdate.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// FieldOperation defined from schema:
// {
// "title": "Field Operation",
// "type": "object"
// }
type FieldOperation map[string]interface{}
+21
View File
@@ -0,0 +1,21 @@
package jiradata
/////////////////////////////////////////////////////////////////////////
// This Code is Generated by SlipScheme Project:
// https://github.com/coryb/slipscheme
//
// Generated with command:
// slipscheme -dir jiradata -pkg jiradata -overwrite schemas/IssueUpdate.json
/////////////////////////////////////////////////////////////////////////
// DO NOT EDIT //
/////////////////////////////////////////////////////////////////////////
// FieldOperations defined from schema:
// {
// "type": "array",
// "items": {
// "title": "Field Operation",
// "type": "object"
// }
// }
type FieldOperations []FieldOperation

Some files were not shown because too many files have changed in this diff Show More