mirror of
https://github.com/Threnklyn/jira.git
synced 2026-05-19 20:53:27 +02:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b5f0d5ed4 | |||
| ddee8a58a9 | |||
| ee69afadd0 | |||
| a87bdf4038 | |||
| 68c2588153 | |||
| 0cba806942 | |||
| df9dbe65b4 | |||
| 492754f059 | |||
| 97d8c5f6e0 | |||
| 1e619ea690 | |||
| b7d8a9c324 | |||
| 8b717de870 | |||
| f5921077ca | |||
| c9a4e30606 | |||
| ef9b731bac | |||
| 62303ed81b | |||
| 7191c7751b | |||
| d16bcc2f51 | |||
| 07ba74b34a | |||
| 462ef1c485 | |||
| eead13aef1 | |||
| 213a7e04af | |||
| 49e44670d9 | |||
| 5503e53168 | |||
| 9ebd2cd64e | |||
| 84b6155b0d | |||
| c23ad75957 | |||
| 3c478004d2 | |||
| ed42ef920a | |||
| fa01ff5c46 | |||
| b98da3612d | |||
| 7f9595cf15 | |||
| 09584981b6 | |||
| 1bc6b55b85 | |||
| d787ac030c | |||
| 09a61c3ea1 | |||
| 64ce3812a6 | |||
| 9146346e2f | |||
| e639cce9af | |||
| 06b26c9e00 | |||
| ac39f9ae1d | |||
| bd3cf994b8 | |||
| 91059b3578 | |||
| 4b9873b323 | |||
| cd106df78a | |||
| 50b5360cfe | |||
| 359bec2fdf | |||
| 79c83f6911 | |||
| 585382eaea | |||
| 9c818d427c | |||
| 8621d9e698 | |||
| 5610707c30 | |||
| 0b4e16a35d | |||
| 57bc97a378 | |||
| 2d02cf8132 | |||
| 18a687e78a | |||
| 5d058536d2 | |||
| d4153be0ec | |||
| edb06621f8 | |||
| 161a68920d | |||
| fd30bc1392 | |||
| 84f77be87c | |||
| dea794f037 | |||
| 43ebc846b1 | |||
| 0d7c1a7931 | |||
| 80325a5955 | |||
| 20a9666fcd | |||
| 4ae760f18f | |||
| 6da9974380 | |||
| 8c7ca383f6 | |||
| 425fa63d33 | |||
| 464742c9ba | |||
| 7fbd87289f | |||
| d400b58019 | |||
| d4a3af862d | |||
| 042bc48649 | |||
| a2e36e808a | |||
| 84bd64a188 | |||
| efbd1dd96d | |||
| ff985f910b | |||
| 9597f9b56f | |||
| 66c069e3b4 | |||
| 14189c197b | |||
| c9b5054cde | |||
| f23b1c4370 | |||
| 6c742dad0a | |||
| 3966defc53 | |||
| 794f8dd259 | |||
| 41d1a7c9a1 | |||
| 90007771bf | |||
| 7bfa241547 | |||
| e6600cf1a5 | |||
| 2e608207cb | |||
| bc1b994019 | |||
| fd399d817e | |||
| f7b587ee91 | |||
| de69971c1c | |||
| 2f9b8bb5c1 | |||
| 093c510ca2 | |||
| d3e294e1ce | |||
| 4ed8edbd19 | |||
| 28d92eb659 | |||
| d16db04e58 | |||
| 4d74554300 |
@@ -3,6 +3,7 @@ config:
|
||||
password-source: pass
|
||||
endpoint: https://go-jira.atlassian.net
|
||||
user: admin
|
||||
login: atlassian@corybennett.org
|
||||
|
||||
queries:
|
||||
todo: |
|
||||
|
||||
+6
-3
@@ -5,12 +5,15 @@ before_install:
|
||||
language: go
|
||||
go_import_path: gopkg.in/Netflix-Skunkworks/go-jira.v1
|
||||
go:
|
||||
- 1.8
|
||||
- 1.9
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
script:
|
||||
- make vet
|
||||
- go get -t -v ./...
|
||||
- go test ./...
|
||||
- go vet -composites=false ./...
|
||||
- make
|
||||
- make prove 2>&1
|
||||
- make prove 2>&1
|
||||
|
||||
|
||||
@@ -1,5 +1,73 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.20 - 2018-08-04
|
||||
|
||||
* [[#201](https://github.com/Netflix-Skunkworks/go-jira/issues/201)] update required library, failing to populate cookiejar from cookies file [Cory Bennett] [[ee69afa](https://github.com/Netflix-Skunkworks/go-jira/commit/ee69afa)]
|
||||
|
||||
## 1.0.19 - 2018-08-02
|
||||
|
||||
* [[#199](https://github.com/Netflix-Skunkworks/go-jira/issues/199)] [[#198](https://github.com/Netflix-Skunkworks/go-jira/issues/198)] update http client library, fix issues with internal login retries [Cory Bennett] [[0cba806](https://github.com/Netflix-Skunkworks/go-jira/commit/0cba806)]
|
||||
|
||||
## 1.0.18 - 2018-07-29
|
||||
|
||||
* [[#196](https://github.com/Netflix-Skunkworks/go-jira/issues/196)] add `jira session` command to show session information if user is authenticated [Cory Bennett] [[f592107](https://github.com/Netflix-Skunkworks/go-jira/commit/f592107)]
|
||||
* [[#192](https://github.com/Netflix-Skunkworks/go-jira/issues/192)] fix template usage for 'fixVersions' in transition template [Cory Bennett] [[c9a4e30](https://github.com/Netflix-Skunkworks/go-jira/commit/c9a4e30)]
|
||||
* move HandleExit to the jiracli package [Cory Bennett] [[ef9b731](https://github.com/Netflix-Skunkworks/go-jira/commit/ef9b731)]
|
||||
* they broke golang.org/x/net: ERROR: vendor/golang.org/x/net/proxy/socks5.go:11:2: use of internal package not allowed [Cory Bennett] [[7191c77](https://github.com/Netflix-Skunkworks/go-jira/commit/7191c77)]
|
||||
* udpate deps [Cory Bennett] [[d16bcc2](https://github.com/Netflix-Skunkworks/go-jira/commit/d16bcc2)]
|
||||
* udpate for figtree api changes [Cory Bennett] [[07ba74b](https://github.com/Netflix-Skunkworks/go-jira/commit/07ba74b)]
|
||||
* udpate to use latest dep, update figtree [Cory Bennett] [[462ef1c](https://github.com/Netflix-Skunkworks/go-jira/commit/462ef1c)]
|
||||
* [[#171](https://github.com/Netflix-Skunkworks/go-jira/issues/171)] change proposed PasswordPath to PasswordName [Cory Bennett] [[213a7e0](https://github.com/Netflix-Skunkworks/go-jira/commit/213a7e0)]
|
||||
* add pass path to config [dvogt23] [[fa01ff5](https://github.com/Netflix-Skunkworks/go-jira/commit/fa01ff5)]
|
||||
|
||||
## 1.0.17 - 2018-04-15
|
||||
|
||||
* fix IsTerminal usage for windows [Cory Bennett] [[7f9595c](https://github.com/Netflix-Skunkworks/go-jira/commit/7f9595c)]
|
||||
* [[#166](https://github.com/Netflix-Skunkworks/go-jira/issues/166)] fix issue when editing templates specified with full path [Cory Bennett] [[d787ac0](https://github.com/Netflix-Skunkworks/go-jira/commit/d787ac0)]
|
||||
* only prompt on logout if stdin and stdout are terminals [Cory Bennett] [[09a61c3](https://github.com/Netflix-Skunkworks/go-jira/commit/09a61c3)]
|
||||
* [[#163](https://github.com/Netflix-Skunkworks/go-jira/issues/163)] fix url path join logic [Cory Bennett] [[9146346](https://github.com/Netflix-Skunkworks/go-jira/commit/9146346)]
|
||||
* [[#160](https://github.com/Netflix-Skunkworks/go-jira/issues/160)] prompt when api-token is invalid to get new token [Cory Bennett] [[e639cce](https://github.com/Netflix-Skunkworks/go-jira/commit/e639cce)]
|
||||
* [[#157](https://github.com/Netflix-Skunkworks/go-jira/issues/157)] add `password-directory: path` to allow overriding PASSWORD_STORE_DIR from configs [Cory Bennett] [[06b26c9](https://github.com/Netflix-Skunkworks/go-jira/commit/06b26c9)]
|
||||
* [[#160](https://github.com/Netflix-Skunkworks/go-jira/issues/160)] allow `jira logout` to delete your api-token from keychain [Cory Bennett] [[bd3cf99](https://github.com/Netflix-Skunkworks/go-jira/commit/bd3cf99)]
|
||||
|
||||
## 1.0.16 - 2018-04-01
|
||||
|
||||
* [[#159](https://github.com/Netflix-Skunkworks/go-jira/issues/159)] fix `slice bounds out of range` error in `abbrev` template function [Cory Bennett] [[359bec2](https://github.com/Netflix-Skunkworks/go-jira/commit/359bec2)]
|
||||
* [[#158](https://github.com/Netflix-Skunkworks/go-jira/issues/158)] always print usage to stdout [Cory Bennett] [[79c83f6](https://github.com/Netflix-Skunkworks/go-jira/commit/79c83f6)]
|
||||
|
||||
## 1.0.15 - 2018-03-08
|
||||
|
||||
* [[#147](https://github.com/Netflix-Skunkworks/go-jira/issues/147)] [[#148](https://github.com/Netflix-Skunkworks/go-jira/issues/148)] add support for api token based authentication [Cory Bennett] [[edb0662](https://github.com/Netflix-Skunkworks/go-jira/commit/edb0662)]
|
||||
* refactor to simplify main [Cory Bennett] [[43ebc84](https://github.com/Netflix-Skunkworks/go-jira/commit/43ebc84)] [[0d7c1a7](https://github.com/Netflix-Skunkworks/go-jira/commit/0d7c1a7)]
|
||||
* [[#145](https://github.com/Netflix-Skunkworks/go-jira/issues/145)] fix to match AuthProvider interface [Cory Bennett] [[80325a5](https://github.com/Netflix-Skunkworks/go-jira/commit/80325a5)]
|
||||
* [[#141](https://github.com/Netflix-Skunkworks/go-jira/issues/141)] better handling in responseError for non-json error responses [Cory Bennett] [[20a9666](https://github.com/Netflix-Skunkworks/go-jira/commit/20a9666)]
|
||||
* Update unexportTemplates.go [GitHub] [[6da9974](https://github.com/Netflix-Skunkworks/go-jira/commit/6da9974)]
|
||||
* [[#139](https://github.com/Netflix-Skunkworks/go-jira/issues/139)] add shellquote and toMinJson template functions [Cory Bennett] [[8c7ca38](https://github.com/Netflix-Skunkworks/go-jira/commit/8c7ca38)]
|
||||
* [[#137](https://github.com/Netflix-Skunkworks/go-jira/issues/137)] update kingpeon dep to allow access to dynamic command structure [Cory Bennett] [[425fa63](https://github.com/Netflix-Skunkworks/go-jira/commit/425fa63)]
|
||||
* field name is "comment" not "comments" [Cory Bennett] [[464742c](https://github.com/Netflix-Skunkworks/go-jira/commit/464742c)]
|
||||
|
||||
## 1.0.14 - 2017-11-04
|
||||
|
||||
* [[#131](https://github.com/Netflix-Skunkworks/go-jira/issues/131)] fix parsing global options before command execution (allow unixproxy/socksproxy to be set in config.yml) [Cory Bennett] [[042bc48](https://github.com/Netflix-Skunkworks/go-jira/commit/042bc48)]
|
||||
* add/update deps [Cory Bennett] [[a2e36e8](https://github.com/Netflix-Skunkworks/go-jira/commit/a2e36e8)]
|
||||
* add support for using socks proxy [onionjake] [[ff985f9](https://github.com/Netflix-Skunkworks/go-jira/commit/ff985f9)]
|
||||
|
||||
## 1.0.13 - 2017-10-28
|
||||
|
||||
* fix transition command [Cory Bennett] [[9597f9b](https://github.com/Netflix-Skunkworks/go-jira/commit/9597f9b)]
|
||||
* fix default values to load after parsing configs [Cory Bennett] [[c9b5054](https://github.com/Netflix-Skunkworks/go-jira/commit/c9b5054)]
|
||||
* add test to make sure IssueType.Fields does not disappear on regeneration [Cory Bennett] [[3966def](https://github.com/Netflix-Skunkworks/go-jira/commit/3966def)]
|
||||
* add tests for validating changes to auto-generated jiradata files [Cory Bennett] [[41d1a7c](https://github.com/Netflix-Skunkworks/go-jira/commit/41d1a7c)]
|
||||
* Fix typo in 'logout' command help [Cory Bennett] [[9000777](https://github.com/Netflix-Skunkworks/go-jira/commit/9000777)]
|
||||
* Add URL escaping to an additional issuetype call [Cory Bennett] [[7bfa241](https://github.com/Netflix-Skunkworks/go-jira/commit/7bfa241)]
|
||||
* Add --resolution option [Cory Bennett] [[e6600cf](https://github.com/Netflix-Skunkworks/go-jira/commit/e6600cf)]
|
||||
* Create Metadata Not Populated Correctly [Dillon Buchanan] [[093c510](https://github.com/Netflix-Skunkworks/go-jira/commit/093c510)]
|
||||
* add regexReplace template function [Dirk Heilig] [[d3e294e](https://github.com/Netflix-Skunkworks/go-jira/commit/d3e294e)]
|
||||
|
||||
## 1.0.12 - 2017-10-04
|
||||
|
||||
* add `{{env.VARNAME}}` template support to allow use of env vars [Cory Bennett] [[4d74554](https://github.com/Netflix-Skunkworks/go-jira/commit/4d74554)]
|
||||
|
||||
## 1.0.11 - 2017-09-26
|
||||
|
||||
* [[#115](https://github.com/Netflix-Skunkworks/go-jira/issues/115)] fix transition template for description [Cory Bennett] [[986cc78](https://github.com/Netflix-Skunkworks/go-jira/commit/986cc78)]
|
||||
|
||||
Generated
+59
-31
@@ -4,7 +4,10 @@
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/template"
|
||||
packages = [".","parse"]
|
||||
packages = [
|
||||
".",
|
||||
"parse"
|
||||
]
|
||||
revision = "a0175ee3bccc567396460bf5acd36800cb10c49c"
|
||||
|
||||
[[projects]]
|
||||
@@ -17,31 +20,38 @@
|
||||
branch = "master"
|
||||
name = "github.com/cheekybits/genny"
|
||||
packages = ["generic"]
|
||||
revision = "9127e812e1e9e501ce899a18121d316ecb52e4ba"
|
||||
revision = "c546fedd85a9b2291805f7a2933a3564cbdda989"
|
||||
source = "github.com/coryb/genny"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/coryb/figtree"
|
||||
packages = ["."]
|
||||
revision = "c7d8fbf1d7746b5864b8262fabffec813b5a43fa"
|
||||
revision = "071d1ef303dfb7738166ba62aac71e5ee10ce218"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/coryb/kingpeon"
|
||||
packages = ["."]
|
||||
revision = "64b561ae2d0f895b94719c486bed798f4236a4b3"
|
||||
revision = "9a669f143f2e7454e80064c47365d139420a3fff"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/coryb/oreo"
|
||||
packages = ["."]
|
||||
revision = "95687d61c95ee1522c1140e2af59b0c1846abfc1"
|
||||
revision = "3e1b88fc08f134aa91ccfb5d58c983ca8ab42589"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/fatih/camelcase"
|
||||
packages = ["."]
|
||||
revision = "f6a740d52f961c60348ebb109adde9f4635d7540"
|
||||
revision = "44e46d280b43ec1531bb25252440e34f1b800b65"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -53,25 +63,25 @@
|
||||
branch = "master"
|
||||
name = "github.com/jinzhu/copier"
|
||||
packages = ["."]
|
||||
revision = "32e0d0db1dcd4373fb9eb0f9d727b1fe1a723e54"
|
||||
revision = "7e38e58719c33e0d44d585c4ab477a30f8cb82dd"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kballard/go-shellquote"
|
||||
packages = ["."]
|
||||
revision = "cd60e84ee657ff3dc51de0b4f55dd299a3e136f2"
|
||||
revision = "95032a82bc518f77982ea72343cc1ade730072f0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
revision = "ad5389df28cdac544c99bd7b9161a0b5b6ca9d1b"
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe"
|
||||
version = "v0.0.2"
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -92,64 +102,82 @@
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/sethgrid/pester"
|
||||
packages = ["."]
|
||||
revision = "a86a2d88f4dc3c7dbf3a6a6bbbfb095690b834b6"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/theckman/go-flock"
|
||||
packages = ["."]
|
||||
revision = "6de226b0d5f040ed85b88c82c381709b98277f3d"
|
||||
revision = "b139a2487364247d91814e4a7c7b8fdc69e342b2"
|
||||
version = "v0.4.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/gjson"
|
||||
packages = ["."]
|
||||
revision = "be96719f990978a867f52c48f29d43f6b591da28"
|
||||
revision = "ba784d767ac7d937cf2439f237e50ec04a381c8b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/match"
|
||||
packages = ["."]
|
||||
revision = "173748da739a410c5b0b813b956f89ff94730b4c"
|
||||
revision = "1731857f09b1f38450e2c12409748407822dc6be"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/tmc/keyring"
|
||||
packages = ["."]
|
||||
revision = "06e6283d50adc5f8fcdb3cdf33ee1244d4400ae1"
|
||||
revision = "839169085ae146fc7a34bcb34dfd7ab216d23991"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["ssh/terminal"]
|
||||
revision = "9ba3862cf6a5452ae579de98f9364dd2e544844c"
|
||||
revision = "c126467f60eb25f8f27e5a981f32a87e3965053f"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/net"
|
||||
packages = ["proxy"]
|
||||
revision = "01c190206fbdffa42f334f4b2bf2220f50e64920"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix","windows"]
|
||||
revision = "a5054c7c1385fd50d9394475365355a87a7873ec"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows"
|
||||
]
|
||||
revision = "bd9dbc187b6e1dacfdd2722a87e83093c2d7bd6e"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/AlecAivazis/survey.v1"
|
||||
packages = [".","core","terminal"]
|
||||
revision = "9d910423e24aa6d7c7950160658c295e0734c7e0"
|
||||
version = "1.3.1"
|
||||
packages = [
|
||||
".",
|
||||
"core",
|
||||
"terminal"
|
||||
]
|
||||
revision = "17861e192dc11fd2f5081df1932c94cce262fa1e"
|
||||
version = "v1.6.1"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
packages = ["."]
|
||||
revision = "1087e65c9441605df944fb12c33f0fe7072d18ca"
|
||||
version = "v2.2.5"
|
||||
revision = "947dcec5ba9c011838740e680966fd7087a71d0d"
|
||||
version = "v2.2.6"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/coryb/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "fb7cb9628c6e3bdd76c29fb91798d51a09832470"
|
||||
revision = "0e40e46f7153ceb79ebbfdd075233d57f9611bd1"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/op/go-logging.v1"
|
||||
@@ -160,6 +188,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "1f4b97fcf898a5ef03af5e222686a09328b393e71797d21bf0c37b74d1e74a8e"
|
||||
inputs-digest = "e087b3c5e03a82796f3bbc9d67c366dd794718c12f9ef9252e6172a6344c4fd7"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
+18
-3
@@ -20,40 +20,50 @@
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
non-go = true
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coryb/figtree"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coryb/kingpeon"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/coryb/oreo"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/jinzhu/copier"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/kballard/go-shellquote"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/mgutz/ansi"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/browser"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/savaki/jq"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/tmc/keyring"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/crypto"
|
||||
branch = "master"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/AlecAivazis/survey.v1"
|
||||
@@ -65,6 +75,7 @@
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/coryb/yaml.v2"
|
||||
branch = "v2"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/op/go-logging.v1"
|
||||
@@ -73,3 +84,7 @@
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/gjson"
|
||||
|
||||
[[constraint]]
|
||||
name = "golang.org/x/net"
|
||||
revision = "01c190206fbdffa42f334f4b2bf2220f50e64920"
|
||||
@@ -1,4 +1,5 @@
|
||||
NAME=jira
|
||||
GO?=go
|
||||
|
||||
OS=$(shell uname -s)
|
||||
ifeq ($(filter CYGWIN%,$(OS)),$(OS))
|
||||
@@ -20,17 +21,17 @@ CURVER ?= $(patsubst v%,%,$(shell [ -d .git ] && git describe --abbrev=0 --tags
|
||||
LDFLAGS:= -w
|
||||
|
||||
build:
|
||||
go build -gcflags="-e" -v -ldflags "$(LDFLAGS) -s" -o '$(BIN)' cmd/jira/main.go
|
||||
$(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
|
||||
@$(GO) vet .
|
||||
@$(GO) vet ./jiracli
|
||||
@$(GO) vet ./jiracmd
|
||||
@$(GO) vet ./jiradata
|
||||
@$(GO) vet ./cmd/jira
|
||||
|
||||
lint:
|
||||
@go get github.com/golang/lint/golint
|
||||
@$(GO) get github.com/golang/lint/golint
|
||||
@golint .
|
||||
@golint ./jiracli
|
||||
@golint ./jiracmd
|
||||
@@ -38,10 +39,10 @@ lint:
|
||||
@golint ./cmd/jira
|
||||
|
||||
all:
|
||||
go get -u github.com/karalabe/xgo
|
||||
$(GO) get -u github.com/karalabe/xgo
|
||||
rm -rf dist
|
||||
mkdir -p dist
|
||||
xgo --go 1.9.0 --targets="freebsd/amd64,linux/386,linux/amd64,windows/386,windows/amd64,darwin/amd64" -dest ./dist -ldflags="-w -s" ./cmd/jira
|
||||
xgo --targets="freebsd/amd64,linux/386,linux/amd64,windows/386,windows/amd64,darwin/amd64" -dest ./dist -ldflags="-w -s" ./cmd/jira
|
||||
|
||||
install:
|
||||
${MAKE} GOBIN=$$HOME/bin build
|
||||
@@ -65,11 +66,11 @@ update-changelog:
|
||||
mv CHANGELOG.md.new CHANGELOG.md; \
|
||||
$(NULL)
|
||||
|
||||
update-usage:
|
||||
@perl -pi -e 'undef $$/; s|\n```\nusage.*?```|"\n```\n".qx{./jira --help}."```"|esg' README.md
|
||||
|
||||
release:
|
||||
perl -pi -e 'undef $$/; s/\n```\nusage.*```//sg' README.md
|
||||
echo '```' >> README.md
|
||||
./jira --help >> README.md 2>&1 || true
|
||||
echo '```' >> README.md
|
||||
make update-usage
|
||||
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
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
* [Install](#install)
|
||||
* [Download](#download)
|
||||
* [Build](#build)
|
||||
* [Usage](#usage)
|
||||
* [TAB completion](#setting-up-tab-completion)
|
||||
* [v1 vs v0 changes](#v1-vs-v0-changes)
|
||||
* [<strong>Golang library import</strong>](#golang-library-import)
|
||||
* [<strong>Configs per command</strong>](#configs-per-command)
|
||||
@@ -27,9 +29,9 @@
|
||||
* [Templates](#templates)
|
||||
* [Writing/Editing Templates](#writingediting-templates)
|
||||
* [Authentication](#authentication)
|
||||
* [user vs login](#user-vs-login)
|
||||
* [keyring password source](#keyring-password-source)
|
||||
* [pass password source](#pass-password-source)
|
||||
* [Usage](#usage)
|
||||
|
||||
# go-jira
|
||||
simple command line client for Atlassian's Jira service written in Go
|
||||
@@ -42,11 +44,114 @@ You can download one of the pre-built binaries for **go-jira** [here](https://gi
|
||||
|
||||
### Build
|
||||
|
||||
You can build and install with [Go](https://golang.org/dl/):
|
||||
You can build and install the official repository with [Go](https://golang.org/dl/):
|
||||
|
||||
```
|
||||
go get gopkg.in/Netflix-Skunkworks/go-jira.v1/cmd/jira
|
||||
```
|
||||
This will checkout this repository into `$GOPATH/src/gopkg.in/Netflix-Skunkworks/go-jira.v1`, build, and install it.
|
||||
|
||||
Because golang likes fully qualified import paths, forking and contributing can be a bit tricky.
|
||||
|
||||
If you want to tinker or hack on go-jira, the [easiest way to do so](http://code.openark.org/blog/development/forking-golang-repositories-on-github-and-managing-the-import-path) is to fork the repository and clone directly into the official path like this:
|
||||
|
||||
`git clone https://github.com/YOUR_USER_NAME_HERE/go-jira $GOPATH/src/gopkg.in/Netflix-Skunkworks/go-jira.v1`
|
||||
|
||||
From within that source dir you can build and install modifications from within that directory like:
|
||||
|
||||
`go install ./...`
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
#### Setting up TAB completion
|
||||
|
||||
Since go-jira is build with the "kingpin" golang command line library we supports bash/zsh shell completion automatically:
|
||||
|
||||
* <https://github.com/alecthomas/kingpin/tree/v2.2.5#bashzsh-shell-completion>
|
||||
|
||||
For example, in bash, adding something along the lines of:
|
||||
|
||||
`eval "$(jira --completion-script-bash)"`
|
||||
|
||||
to your bashrc, or .profile (assuming go-jira binary is already in your path) will cause jira to offer tab completion behavior.
|
||||
|
||||
```
|
||||
usage: jira [<flags>] <command> [<args> ...]
|
||||
|
||||
Jira Command Line Interface
|
||||
|
||||
Global 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
|
||||
--socksproxy=SOCKSPROXY Address for a socks proxy
|
||||
-u, --user=USER user name used within the Jira service
|
||||
--login=LOGIN login name that corresponds to the user used for authentication
|
||||
|
||||
Commands:
|
||||
help: Show help.
|
||||
version: Prints version
|
||||
acknowledge: Transition issue to acknowledge state
|
||||
assign: Assign user to issue
|
||||
attach create: Attach file to issue
|
||||
attach get: Fetch attachment
|
||||
attach list: Prints attachment details for issue
|
||||
attach remove: Delete attachment
|
||||
backlog: Transition issue to Backlog state
|
||||
block: Mark issues as blocker
|
||||
browse: Open issue in browser
|
||||
close: Transition issue to close state
|
||||
comment: Add comment to issue
|
||||
component add: Add component
|
||||
components: Show components for a project
|
||||
create: Create issue
|
||||
createmeta: View 'create' metadata
|
||||
done: Transition issue to Done state
|
||||
dup: Mark issues as duplicate
|
||||
edit: Edit issue details
|
||||
editmeta: View 'edit' metadata
|
||||
epic add: Add issues to Epic
|
||||
epic create: Create Epic
|
||||
epic list: Prints list of issues for an epic with optional search criteria
|
||||
epic remove: Remove issues from Epic
|
||||
export-templates: Export templates for customizations
|
||||
fields: Prints all fields, both System and Custom
|
||||
in-progress: Transition issue to Progress state
|
||||
issuelink: Link two issues
|
||||
issuelinktypes: Show the issue link types
|
||||
issuetypes: Show issue types for a project
|
||||
labels add: Add labels to an issue
|
||||
labels remove: Remove labels from an issue
|
||||
labels set: Set labels on an issue
|
||||
list: Prints list of issues for given search criteria
|
||||
login: Attempt to login into jira server
|
||||
logout: Deactivate session with Jira server
|
||||
rank: Mark issues as blocker
|
||||
reopen: Transition issue to reopen state
|
||||
request: Open issue in requestr
|
||||
resolve: Transition issue to resolve state
|
||||
start: Transition issue to start state
|
||||
stop: Transition issue to stop state
|
||||
subtask: Subtask issue
|
||||
take: Assign issue to yourself
|
||||
todo: Transition issue to To Do state
|
||||
transition: Transition issue to given state
|
||||
transitions: List valid issue transitions
|
||||
transmeta: List valid issue transitions
|
||||
unassign: Unassign an issue
|
||||
unexport-templates: Remove unmodified exported templates
|
||||
view: Prints issue details
|
||||
vote: Vote up/down an issue
|
||||
watch: Add/Remove watcher to issue
|
||||
worklog add: Add a worklog to an issue
|
||||
worklog list: Prints the worklog data for given issue
|
||||
session: Attempt to login into jira server
|
||||
|
||||
```
|
||||
|
||||
## v1 vs v0 changes
|
||||
|
||||
@@ -62,7 +167,7 @@ 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:
|
||||
Instead of requiring a executable 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 your `jira list` you can now do:
|
||||
```
|
||||
$ cat $HOME/.jira.d/list.yml
|
||||
template: table
|
||||
@@ -71,7 +176,7 @@ Where previously you needed something like:
|
||||
```
|
||||
# cat $HOME/.jira.d/config.yml
|
||||
#!/bin/sh
|
||||
case $JIRA_OPERATION in
|
||||
case $JIRA_OPERATION in
|
||||
list)
|
||||
echo "template: table";;
|
||||
esac
|
||||
@@ -79,7 +184,7 @@ 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:
|
||||
```
|
||||
```yaml
|
||||
custom-commands:
|
||||
- name: mine
|
||||
help: display issues assigned to me
|
||||
@@ -103,7 +208,7 @@ Flags:
|
||||
```
|
||||
|
||||
###### **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:
|
||||
Unfortunately during the rewrite between v0 and v1 there were some necessary changes 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`
|
||||
@@ -112,22 +217,19 @@ Unfortunately during the rewrite between v0 and v1 there were some changes neces
|
||||
* `jira set labels` => `jira labels set`
|
||||
|
||||
###### **Login process change**
|
||||
We have, once again, changed how login happens for Jira. When authenticating against Atlassian Cloud Jira [API Tokens are now required](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/). Please read [the Authentication section](#authentication) below for more information.
|
||||
|
||||
If you use a privately hosted Jira service, you can chose to use the API Token method or continue using the session login api. Please read [the Authentication section](#authentication) below for more information.
|
||||
|
||||
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 **<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.
|
||||
**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 **<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 closest to your current working directory will have precedence. Properties overridden with command line options will have final precedence.
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```
|
||||
```yaml
|
||||
project: foo
|
||||
```
|
||||
|
||||
@@ -153,7 +255,7 @@ If the **.jira.d/config.yml** file is executable, then **go-jira** will attempt
|
||||
echo "endpoint: https://jira.mycompany.com"
|
||||
echo "editor: emacs -nw"
|
||||
|
||||
case $JIRA_OPERATION in
|
||||
case $JIRA_OPERATION in
|
||||
list)
|
||||
echo "template: table";;
|
||||
esac
|
||||
@@ -175,7 +277,7 @@ 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:
|
||||
```
|
||||
```yaml
|
||||
custom-commands:
|
||||
- command1
|
||||
- command2
|
||||
@@ -188,14 +290,14 @@ Where the individual commands are maps with these keys:
|
||||
* `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.
|
||||
* `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:
|
||||
* `help: string` The help message 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-explanatory. 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`.
|
||||
@@ -208,9 +310,9 @@ These are possible keys under the command `options` property:
|
||||
|
||||
##### 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:
|
||||
* `name: string` [**required**] Name of the option, so `name: ISSUE` will show in the usage 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 message displayed in usage for the argument.
|
||||
* `type: string`: The type of the argument, 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-explanatory. 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`.
|
||||
@@ -220,10 +322,10 @@ These are possible keys under the command `args` property:
|
||||
* `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.
|
||||
The `script` property is a template that would 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:
|
||||
```
|
||||
```yaml
|
||||
custom-commands:
|
||||
- name: custom-test
|
||||
help: Testing the custom commands
|
||||
@@ -274,16 +376,16 @@ 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:
|
||||
```
|
||||
```yaml
|
||||
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:
|
||||
```
|
||||
```yaml
|
||||
project: PROJECT
|
||||
custom-commands:
|
||||
- name: print-project
|
||||
@@ -294,7 +396,7 @@ custom-commands:
|
||||
##### Examples
|
||||
|
||||
* `jira mine` for listing issues assigned to you
|
||||
```
|
||||
```yaml
|
||||
custom-commands:
|
||||
- name: mine
|
||||
help: display issues assigned to me
|
||||
@@ -308,7 +410,7 @@ custom-commands:
|
||||
fi
|
||||
```
|
||||
* `jira sprint` for listing issues in your current sprint
|
||||
```
|
||||
```yaml
|
||||
custom-commands:
|
||||
- name: sprint
|
||||
help: display issues for active sprint
|
||||
@@ -325,7 +427,7 @@ custom-commands:
|
||||
### 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
|
||||
closes **go-jira** will submit the completed form. The order which **go-jira** attempts to determine your prefered editor is:
|
||||
closes **go-jira** will submit the completed form. The order which **go-jira** attempts to determine your preferred editor is:
|
||||
|
||||
* **editor** property in any config.yml file
|
||||
* **JIRA_EDITOR** environment variable
|
||||
@@ -344,9 +446,9 @@ hard-coded templates with `jira export-templates` which will write them to **~/.
|
||||
|
||||
#### 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).
|
||||
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 easier 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:
|
||||
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 example to find out what is available to the "view" templates, you can use:
|
||||
```
|
||||
jira view GOJIRA-321 -t debug
|
||||
```
|
||||
@@ -358,7 +460,18 @@ 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`.
|
||||
For Atlassian Cloud hosted Jira [API Tokens are now required](https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/). You will automatically be prompted for an API Token if your jira endpoint ends in `.atlassian.net`. If you are using a private Jira service, you can force `jira` to use an api-token by setting the `authentication-method: api-token` property in your `$HOME/.jira.d/config.yml` file. The API Token needs to be presented to the Jira service on every request, so it is recommended to store this API Token security within your OS's keyring, or using the `pass` service as documented below so that it can be programmatically accessed via `jira` and not prompt you every time. For a less-secure option you can also provide the API token via a `JIRA_API_TOKEN` environment variable. If you are unable to use an api-token for an Atlassian Cloud hosted Jira then you can still force `jira` to use the old session based authentication (until it the hosted system stops accepting it) by setting `authentication-method: session`.
|
||||
|
||||
If your Jira service still allows you to use the Session based authentication method then `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`.
|
||||
|
||||
#### User vs Login
|
||||
The Jira service has sometimes differing opinions about how a user is identified. In other words the ID you login with might not be ID that the jira system recognized you as. This matters when trying to identify a user via various Jira REST APIs (like issue assignment). This is especially relevant when trying to authenticate with an API Token where the authentication user is usually an email address, but within the Jira system the user is identified by a user name. To accommodate this `jira` now supports two different properties in the config file. So when authentication using the API Tokens you will likely want something like this in your `$HOME/.jira.d/config.yml` file:
|
||||
```yaml
|
||||
user: person
|
||||
login: person@example.com
|
||||
```
|
||||
|
||||
You can also override these values on the command line with `jira --user person --login person@example.com`. The `login` value will be used only for authentication purposes, the `user` value will be used when a user name is required for any Jira service API calls.
|
||||
|
||||
#### 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`:
|
||||
@@ -394,7 +507,7 @@ 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`
|
||||
You probably want to setup gpg-agent so that you don't 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
|
||||
@@ -419,79 +532,3 @@ if [ -n "${GPG_AGENT_INFO}" ]; then
|
||||
fi
|
||||
export GPG_TTY=$(tty)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
usage: jira [<flags>] <command> [<args> ...]
|
||||
|
||||
Jira Command Line Interface
|
||||
|
||||
Global 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
|
||||
|
||||
Commands:
|
||||
help: Show help.
|
||||
version: Prints version
|
||||
acknowledge: Transition issue to acknowledge state
|
||||
assign: Assign user to issue
|
||||
attach create: Attach file to issue
|
||||
attach get: Fetch attachment
|
||||
attach list: Prints attachment details for issue
|
||||
attach remove: Delete attachment
|
||||
backlog: Transition issue to Backlog state
|
||||
block: Mark issues as blocker
|
||||
browse: Open issue in browser
|
||||
close: Transition issue to close state
|
||||
comment: Add comment to issue
|
||||
component add: Add component
|
||||
components: Show components for a project
|
||||
create: Create issue
|
||||
createmeta: View 'create' metadata
|
||||
done: Transition issue to Done state
|
||||
dup: Mark issues as duplicate
|
||||
edit: Edit issue details
|
||||
editmeta: View 'edit' metadata
|
||||
epic add: Add issues to Epic
|
||||
epic create: Create Epic
|
||||
epic list: Prints list of issues for an epic with optional search criteria
|
||||
epic remove: Remove issues from Epic
|
||||
export-templates: Export templates for customizations
|
||||
fields: Prints all fields, both System and Custom
|
||||
in-progress: Transition issue to Progress state
|
||||
issuelink: Link two issues
|
||||
issuelinktypes: Show the issue link types
|
||||
issuetypes: Show issue types for a project
|
||||
labels add: Add labels to an issue
|
||||
labels remove: Remove labels from an issue
|
||||
labels set: Set labels on an issue
|
||||
list: Prints list of issues for given search criteria
|
||||
login: Attempt to login into jira server
|
||||
logout: Deactivate sesssion with Jira server
|
||||
rank: Mark issues as blocker
|
||||
reopen: Transition issue to reopen state
|
||||
request: Open issue in requestr
|
||||
resolve: Transition issue to resolve state
|
||||
start: Transition issue to start state
|
||||
stop: Transition issue to stop state
|
||||
subtask: Subtask issue
|
||||
take: Assign issue to yourself
|
||||
todo: Transition issue to To Do state
|
||||
transition: Transition issue to given state
|
||||
transitions: List valid issue transitions
|
||||
transmeta: List valid issue transitions
|
||||
unassign: Unassign an issue
|
||||
unexport-templates: Remove unmodified exported templates
|
||||
view: Prints issue details
|
||||
vote: Vote up/down an issue
|
||||
watch: Add/Remove watcher to issue
|
||||
worklog add: Add a worklog to an issue
|
||||
worklog list: Prints the worklog data for given issue
|
||||
|
||||
```
|
||||
|
||||
+2
-4
@@ -1,8 +1,6 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
)
|
||||
|
||||
@@ -12,7 +10,7 @@ func (j *Jira) GetAttachment(id string) (*jiradata.Attachment, error) {
|
||||
}
|
||||
|
||||
func GetAttachment(ua HttpClient, endpoint string, id string) (*jiradata.Attachment, error) {
|
||||
uri := fmt.Sprintf("%s/rest/api/2/attachment/%s", endpoint, id)
|
||||
uri := URLJoin(endpoint, "rest/api/2/attachment", id)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -32,7 +30,7 @@ func (j *Jira) RemoveAttachment(id string) error {
|
||||
}
|
||||
|
||||
func RemoveAttachment(ua HttpClient, endpoint string, id string) error {
|
||||
uri := fmt.Sprintf("%s/rest/api/2/attachment/%s", endpoint, id)
|
||||
uri := URLJoin(endpoint, "rest/api/2/attachment", id)
|
||||
resp, err := ua.Delete(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+20
-294
@@ -1,322 +1,48 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
type oreoLogger struct {
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
func increaseLogLevel(verbosity int) {
|
||||
logging.SetLevel(logging.GetLevel("")+logging.Level(verbosity), "")
|
||||
if logging.GetLevel("") > logging.DEBUG {
|
||||
oreo.TraceRequestBody = true
|
||||
oreo.TraceResponseBody = true
|
||||
}
|
||||
var log = logging.MustGetLogger("jira")
|
||||
|
||||
func (ol *oreoLogger) Printf(format string, args ...interface{}) {
|
||||
ol.logger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
var usage = `{{define "FormatCommand"}}\
|
||||
{{if .FlagSummary}} {{.FlagSummary}}{{end}}\
|
||||
{{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\
|
||||
{{end}}\
|
||||
|
||||
{{define "FormatBriefCommands"}}\
|
||||
{{range .FlattenedCommands}}\
|
||||
{{if not .Hidden}}\
|
||||
{{ print .FullCommand ":" | printf "%-20s"}} {{.Help}}
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
|
||||
{{define "FormatCommands"}}\
|
||||
{{range .FlattenedCommands}}\
|
||||
{{if not .Hidden}}\
|
||||
{{.FullCommand}}{{if .Default}}*{{end}}{{template "FormatCommand" .}}
|
||||
{{.Help|Wrap 4}}
|
||||
{{with .Flags|FlagsToTwoColumns}}{{FormatTwoColumnsWithIndent . 4 2}}{{end}}
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
|
||||
{{define "FormatUsage"}}\
|
||||
{{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end}}
|
||||
{{if .Help}}
|
||||
{{.Help|Wrap 0}}\
|
||||
{{end}}\
|
||||
|
||||
{{end}}\
|
||||
|
||||
{{if .Context.SelectedCommand}}\
|
||||
usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatCommand" .Context.SelectedCommand}}
|
||||
{{if .Context.SelectedCommand.Aliases }}\
|
||||
{{range $top := .App.Commands}}\
|
||||
{{if eq $top.FullCommand $.Context.SelectedCommand.FullCommand}}\
|
||||
{{range $alias := $.Context.SelectedCommand.Aliases}}\
|
||||
alias: {{$.App.Name}} {{$alias}}{{template "FormatCommand" $.Context.SelectedCommand}}
|
||||
{{end}}\
|
||||
{{else}}\
|
||||
{{range $sub := $top.Commands}}\
|
||||
{{if eq $sub.FullCommand $.Context.SelectedCommand.FullCommand}}\
|
||||
{{range $alias := $.Context.SelectedCommand.Aliases}}\
|
||||
alias: {{$.App.Name}} {{$top.Name}} {{$alias}}{{template "FormatCommand" $.Context.SelectedCommand}}
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}
|
||||
{{if .Context.SelectedCommand.Help}}\
|
||||
{{.Context.SelectedCommand.Help|Wrap 0}}
|
||||
{{end}}\
|
||||
{{else}}\
|
||||
usage: {{.App.Name}}{{template "FormatUsage" .App}}
|
||||
{{end}}\
|
||||
|
||||
{{if .App.Flags}}\
|
||||
Global flags:
|
||||
{{.App.Flags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end}}\
|
||||
{{if .Context.SelectedCommand}}\
|
||||
{{if and .Context.SelectedCommand.Flags|RequiredFlags}}\
|
||||
Required flags:
|
||||
{{.Context.SelectedCommand.Flags|RequiredFlags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end}}\
|
||||
{{if .Context.SelectedCommand.Flags|OptionalFlags}}\
|
||||
Optional flags:
|
||||
{{.Context.SelectedCommand.Flags|OptionalFlags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{if .Context.Args}}\
|
||||
Args:
|
||||
{{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}}
|
||||
{{end}}\
|
||||
{{if .Context.SelectedCommand}}\
|
||||
{{if .Context.SelectedCommand.Commands}}\
|
||||
Subcommands:
|
||||
{{template "FormatCommands" .Context.SelectedCommand}}
|
||||
{{end}}\
|
||||
{{else if .App.Commands}}\
|
||||
Commands:
|
||||
{{template "FormatBriefCommands" .App}}
|
||||
{{end}}\
|
||||
`
|
||||
|
||||
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),
|
||||
),
|
||||
defer jiracli.HandleExit()
|
||||
|
||||
jiracli.InitLogging()
|
||||
|
||||
configDir := ".jira.d"
|
||||
fig := figtree.NewFigTree(
|
||||
figtree.WithHome(jiracli.Homedir()),
|
||||
figtree.WithEnvPrefix("JIRA"),
|
||||
figtree.WithConfigDir(configDir),
|
||||
)
|
||||
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})
|
||||
})
|
||||
app.UsageTemplate(usage)
|
||||
|
||||
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 {
|
||||
if err := os.MkdirAll(filepath.Join(jiracli.Homedir(), 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"))
|
||||
o := oreo.New().WithCookieFile(filepath.Join(jiracli.Homedir(), configDir, "cookies.js")).WithLogger(&oreoLogger{log})
|
||||
|
||||
registry := []jiracli.CommandRegistry{
|
||||
{Command: "acknowledge", Entry: jiracmd.CmdTransitionRegistry("acknowledge"), Aliases: []string{"ack"}},
|
||||
{Command: "assign", Entry: jiracmd.CmdAssignRegistry(), Aliases: []string{"give"}},
|
||||
{Command: "attach create", Entry: jiracmd.CmdAttachCreateRegistry()},
|
||||
{Command: "attach get", Entry: jiracmd.CmdAttachGetRegistry()},
|
||||
{Command: "attach list", Entry: jiracmd.CmdAttachListRegistry(), Aliases: []string{"ls"}},
|
||||
{Command: "attach remove", Entry: jiracmd.CmdAttachRemoveRegistry(), Aliases: []string{"rm"}},
|
||||
{Command: "backlog", Entry: jiracmd.CmdTransitionRegistry("Backlog")},
|
||||
{Command: "block", Entry: jiracmd.CmdBlockRegistry()},
|
||||
{Command: "browse", Entry: jiracmd.CmdBrowseRegistry(), Aliases: []string{"b"}},
|
||||
{Command: "close", Entry: jiracmd.CmdTransitionRegistry("close")},
|
||||
{Command: "comment", Entry: jiracmd.CmdCommentRegistry()},
|
||||
{Command: "component add", Entry: jiracmd.CmdComponentAddRegistry()},
|
||||
{Command: "components", Entry: jiracmd.CmdComponentsRegistry()},
|
||||
{Command: "create", Entry: jiracmd.CmdCreateRegistry()},
|
||||
{Command: "createmeta", Entry: jiracmd.CmdCreateMetaRegistry()},
|
||||
{Command: "done", Entry: jiracmd.CmdTransitionRegistry("Done")},
|
||||
{Command: "dup", Entry: jiracmd.CmdDupRegistry()},
|
||||
{Command: "edit", Entry: jiracmd.CmdEditRegistry()},
|
||||
{Command: "editmeta", Entry: jiracmd.CmdEditMetaRegistry()},
|
||||
{Command: "epic add", Entry: jiracmd.CmdEpicAddRegistry()},
|
||||
{Command: "epic create", Entry: jiracmd.CmdEpicCreateRegistry()},
|
||||
{Command: "epic list", Entry: jiracmd.CmdEpicListRegistry(), Aliases: []string{"ls"}},
|
||||
{Command: "epic remove", Entry: jiracmd.CmdEpicRemoveRegistry(), Aliases: []string{"rm"}},
|
||||
{Command: "export-templates", Entry: jiracmd.CmdExportTemplatesRegistry()},
|
||||
{Command: "fields", Entry: jiracmd.CmdFieldsRegistry()},
|
||||
{Command: "in-progress", Entry: jiracmd.CmdTransitionRegistry("Progress"), Aliases: []string{"prog", "progress"}},
|
||||
{Command: "issuelink", Entry: jiracmd.CmdIssueLinkRegistry()},
|
||||
{Command: "issuelinktypes", Entry: jiracmd.CmdIssueLinkTypesRegistry()},
|
||||
{Command: "issuetypes", Entry: jiracmd.CmdIssueTypesRegistry()},
|
||||
{Command: "labels add", Entry: jiracmd.CmdLabelsAddRegistry()},
|
||||
{Command: "labels remove", Entry: jiracmd.CmdLabelsRemoveRegistry(), Aliases: []string{"rm"}},
|
||||
{Command: "labels set", Entry: jiracmd.CmdLabelsSetRegistry()},
|
||||
{Command: "list", Entry: jiracmd.CmdListRegistry(), Aliases: []string{"ls"}},
|
||||
{Command: "login", Entry: jiracmd.CmdLoginRegistry()},
|
||||
{Command: "logout", Entry: jiracmd.CmdLogoutRegistry()},
|
||||
{Command: "rank", Entry: jiracmd.CmdRankRegistry()},
|
||||
{Command: "reopen", Entry: jiracmd.CmdTransitionRegistry("reopen")},
|
||||
{Command: "request", Entry: jiracmd.CmdRequestRegistry(), Aliases: []string{"req"}},
|
||||
{Command: "resolve", Entry: jiracmd.CmdTransitionRegistry("resolve")},
|
||||
{Command: "start", Entry: jiracmd.CmdTransitionRegistry("start")},
|
||||
{Command: "stop", Entry: jiracmd.CmdTransitionRegistry("stop")},
|
||||
{Command: "subtask", Entry: jiracmd.CmdSubtaskRegistry()},
|
||||
{Command: "take", Entry: jiracmd.CmdTakeRegistry()},
|
||||
{Command: "todo", Entry: jiracmd.CmdTransitionRegistry("To Do")},
|
||||
{Command: "transition", Entry: jiracmd.CmdTransitionRegistry(""), Aliases: []string{"trans"}},
|
||||
{Command: "transitions", Entry: jiracmd.CmdTransitionsRegistry("transitions")},
|
||||
{Command: "transmeta", Entry: jiracmd.CmdTransitionsRegistry("debug")},
|
||||
{Command: "unassign", Entry: jiracmd.CmdUnassignRegistry()},
|
||||
{Command: "unexport-templates", Entry: jiracmd.CmdUnexportTemplatesRegistry()},
|
||||
{Command: "view", Entry: jiracmd.CmdViewRegistry()},
|
||||
{Command: "vote", Entry: jiracmd.CmdVoteRegistry()},
|
||||
{Command: "watch", Entry: jiracmd.CmdWatchRegistry()},
|
||||
{Command: "worklog add", Entry: jiracmd.CmdWorklogAddRegistry()},
|
||||
{Command: "worklog list", Entry: jiracmd.CmdWorklogListRegistry(), Default: true},
|
||||
}
|
||||
jiracmd.RegisterAllCommands()
|
||||
|
||||
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})
|
||||
}
|
||||
}
|
||||
app := jiracli.CommandLine(fig, o)
|
||||
jiracli.ParseCommandLine(app, os.Args[1:])
|
||||
}
|
||||
|
||||
+1
-2
@@ -3,7 +3,6 @@ package jira
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
)
|
||||
@@ -23,7 +22,7 @@ func CreateComponent(ua HttpClient, endpoint string, cp ComponentProvider) (*jir
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/api/2/component", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/api/2/component")
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -23,7 +23,7 @@ func EpicSearch(ua HttpClient, endpoint string, epic string, sp SearchProvider)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
uri, err := url.Parse(fmt.Sprintf("%s/rest/agile/1.0/epic/%s/issue", endpoint, epic))
|
||||
uri, err := url.Parse(URLJoin(endpoint, "rest/agile/1.0/epic", epic, "issue"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func EpicAddIssues(ua HttpClient, endpoint string, epic string, eip EpicIssuesPr
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/agile/1.0/epic/%s/issue", endpoint, epic)
|
||||
uri := URLJoin(endpoint, "rest/agile/1.0/epic", epic, "issue")
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -99,7 +99,7 @@ func EpicRemoveIssues(ua HttpClient, endpoint string, eip EpicIssuesProvider) er
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/rest/agile/1.0/epic/none/issue", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/agile/1.0/epic/none/issue")
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
@@ -10,10 +9,12 @@ import (
|
||||
func responseError(resp *http.Response) error {
|
||||
results := &jiradata.ErrorCollection{}
|
||||
if err := readJSON(resp.Body, results); err != nil {
|
||||
return err
|
||||
results.Status = resp.StatusCode
|
||||
results.ErrorMessages = append(results.ErrorMessages, err.Error())
|
||||
}
|
||||
if len(results.ErrorMessages) == 0 && len(results.Errors) == 0 {
|
||||
return fmt.Errorf(resp.Status)
|
||||
results.Status = resp.StatusCode
|
||||
results.ErrorMessages = append(results.ErrorMessages, resp.Status)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
)
|
||||
|
||||
@@ -12,7 +10,7 @@ func (j *Jira) GetFields() ([]jiradata.Field, error) {
|
||||
}
|
||||
|
||||
func GetFields(ua HttpClient, endpoint string) ([]jiradata.Field, error) {
|
||||
uri := fmt.Sprintf("%s/rest/api/2/field", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/api/2/field")
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -59,7 +59,8 @@ func GetIssue(ua HttpClient, endpoint string, issue string, iqg IssueQueryProvid
|
||||
if iqg != nil {
|
||||
query = iqg.ProvideIssueQueryString()
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s%s", endpoint, issue, query)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue)
|
||||
uri += query
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -84,7 +85,8 @@ func GetIssueWorklog(ua HttpClient, endpoint string, issue string) (*jiradata.Wo
|
||||
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)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "worklog")
|
||||
uri += fmt.Sprintf("?startAt=%d&maxResults=%d", startAt, maxResults)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -124,7 +126,7 @@ func AddIssueWorklog(ua HttpClient, endpoint string, issue string, wp WorklogPro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/worklog", endpoint, issue)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "worklog")
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -144,7 +146,7 @@ func (j *Jira) GetIssueEditMeta(issue string) (*jiradata.EditMeta, error) {
|
||||
}
|
||||
|
||||
func GetIssueEditMeta(ua HttpClient, endpoint string, issue string) (*jiradata.EditMeta, error) {
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/editmeta", endpoint, issue)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "editmeta")
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -173,7 +175,7 @@ func EditIssue(ua HttpClient, endpoint string, issue string, iup IssueUpdateProv
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s", endpoint, issue)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue)
|
||||
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -197,7 +199,7 @@ func CreateIssue(ua HttpClient, endpoint string, iup IssueUpdateProvider) (*jira
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue")
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -217,7 +219,8 @@ func (j *Jira) GetIssueCreateMetaProject(projectKey string) (*jiradata.CreateMet
|
||||
}
|
||||
|
||||
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)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue/createmeta")
|
||||
uri += fmt.Sprintf("?projectKeys=%s&expand=projects.issuetypes.fields", projectKey)
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -246,7 +249,8 @@ func (j *Jira) GetIssueCreateMetaIssueType(projectKey, issueTypeName string) (*j
|
||||
}
|
||||
|
||||
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)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue/createmeta")
|
||||
uri += fmt.Sprintf("?projectKeys=%s&issuetypeNames=%s&expand=projects.issuetypes.fields", projectKey, url.QueryEscape(issueTypeName))
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -288,7 +292,7 @@ func LinkIssues(ua HttpClient, endpoint string, lip LinkIssueProvider) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issueLink", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issueLink")
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -307,7 +311,8 @@ func (j *Jira) GetIssueTransitions(issue string) (*jiradata.TransitionsMeta, err
|
||||
}
|
||||
|
||||
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)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "transitions")
|
||||
uri += "?expand=transitions.fields"
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -332,7 +337,7 @@ func TransitionIssue(ua HttpClient, endpoint string, issue string, iup IssueUpda
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/transitions", endpoint, issue)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "transitions")
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -351,7 +356,7 @@ func (j *Jira) GetIssueLinkTypes() (*jiradata.IssueLinkTypes, error) {
|
||||
}
|
||||
|
||||
func GetIssueLinkTypes(ua HttpClient, endpoint string) (*jiradata.IssueLinkTypes, error) {
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issueLinkType", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issueLinkType")
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -375,7 +380,7 @@ func (j *Jira) IssueAddVote(issue string) error {
|
||||
}
|
||||
|
||||
func IssueAddVote(ua HttpClient, endpoint string, issue string) error {
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", endpoint, issue)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "votes")
|
||||
resp, err := ua.Post(uri, "application/json", strings.NewReader("{}"))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -394,7 +399,7 @@ func (j *Jira) IssueRemoveVote(issue string) error {
|
||||
}
|
||||
|
||||
func IssueRemoveVote(ua HttpClient, endpoint string, issue string) error {
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/votes", endpoint, issue)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "votes")
|
||||
resp, err := ua.Delete(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -422,7 +427,7 @@ func RankIssues(ua HttpClient, endpoint string, rrp RankRequestProvider) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/agile/1.0/issue/rank", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/agile/1.0/issue/rank")
|
||||
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -441,7 +446,7 @@ func (j *Jira) IssueAddWatcher(issue, user string) error {
|
||||
}
|
||||
|
||||
func IssueAddWatcher(ua HttpClient, endpoint string, issue, user string) error {
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/watchers", endpoint, issue)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "watchers")
|
||||
resp, err := ua.Post(uri, "application/json", strings.NewReader(fmt.Sprintf("%q", user)))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -460,7 +465,8 @@ func (j *Jira) IssueRemoveWatcher(issue, user string) error {
|
||||
}
|
||||
|
||||
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)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "watchers")
|
||||
uri += fmt.Sprintf("?username=%s", user)
|
||||
resp, err := ua.Delete(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -488,7 +494,7 @@ func IssueAddComment(ua HttpClient, endpoint string, issue string, cp CommentPro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/comment", endpoint, issue)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "comment")
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -526,7 +532,7 @@ func IssueAssign(ua HttpClient, endpoint string, issue, name string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/api/2/issue/%s/assignee", endpoint, issue)
|
||||
uri := URLJoin(endpoint, "rest/api/2/issue", issue, "assignee")
|
||||
resp, err := ua.Put(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -556,7 +562,7 @@ func IssueAttachFile(ua HttpClient, endpoint string, issue, filename string, con
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri, err := url.Parse(fmt.Sprintf("%s/rest/api/2/issue/%s/attachments", endpoint, issue))
|
||||
uri, err := url.Parse(URLJoin(endpoint, "rest/api/2/issue", issue, "attachments"))
|
||||
req := oreo.RequestBuilder(uri).WithMethod("POST").WithHeader(
|
||||
"X-Atlassian-Token", "no-check",
|
||||
).WithHeader(
|
||||
@@ -571,15 +577,6 @@ func IssueAttachFile(ua HttpClient, endpoint string, issue, filename string, con
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
var log = logging.MustGetLogger("jira")
|
||||
|
||||
const VERSION = "1.0.11"
|
||||
const VERSION = "1.0.20"
|
||||
|
||||
type Jira struct {
|
||||
Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||
|
||||
+138
-36
@@ -3,6 +3,7 @@ package jiracli
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/coryb/figtree"
|
||||
@@ -23,19 +25,76 @@ import (
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
var log = logging.MustGetLogger("jira")
|
||||
|
||||
type Exit struct {
|
||||
Code int
|
||||
}
|
||||
|
||||
// HandleExit will unwind any panics and check to see if they are jiracli.Exit
|
||||
// and exit accordingly.
|
||||
//
|
||||
// Example:
|
||||
// func main() {
|
||||
// defer jiracli.HandleExit()
|
||||
// ...
|
||||
// }
|
||||
func HandleExit() {
|
||||
if e := recover(); e != nil {
|
||||
if exit, ok := e.(Exit); ok {
|
||||
os.Exit(exit.Code)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s\n%s", e, debug.Stack())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type GlobalOptions struct {
|
||||
Endpoint figtree.StringOption `yaml:"endpoint,omitempty" json:"endpoint,omitempty"`
|
||||
Insecure figtree.BoolOption `yaml:"insecure,omitempty" json:"insecure,omitempty"`
|
||||
// AuthenticationMethod is the method we use to authenticate with the jira serivce. Possible values are "api-token" or "session".
|
||||
// The default is "api-token" when the service endpoint ends with "atlassian.net", otherwise it "session". Session authentication
|
||||
// will promt for user password and use the /auth/1/session-login endpoint.
|
||||
AuthenticationMethod figtree.StringOption `yaml:"authentication-method,omitempty" json:"authentication-method,omitempty"`
|
||||
|
||||
// Endpoint is the URL for the Jira service. Something like: https://go-jira.atlassian.net
|
||||
Endpoint figtree.StringOption `yaml:"endpoint,omitempty" json:"endpoint,omitempty"`
|
||||
|
||||
// Insecure will allow you to connect to an https endpoint with a self-signed SSL certificate
|
||||
Insecure figtree.BoolOption `yaml:"insecure,omitempty" json:"insecure,omitempty"`
|
||||
|
||||
// Login is the id used for authenticating with the Jira service. For "api-token" AuthenticationMethod this is usually a
|
||||
// full email address, something like "user@example.com". For "session" AuthenticationMethod this will be something
|
||||
// like "user", which by default will use the same value in the `User` field.
|
||||
Login figtree.StringOption `yaml:"login,omitempty" json:"login,omitempty"`
|
||||
|
||||
// PasswordSource specificies the method that we fetch the password. Possible values are "keyring" or "pass".
|
||||
// If this is unset we will just prompt the user. For "keyring" this will look in the OS keychain, if missing
|
||||
// then prompt the user and store the password in the OS keychain. For "pass" this will look in the PasswordDirectory
|
||||
// location using the `pass` tool, if missing prompt the user and store in the PasswordDirectory
|
||||
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"`
|
||||
|
||||
// PasswordDirectory is only used for the "pass" PasswordSource. It is the location for the encrypted password
|
||||
// files used by `pass`. Effectively this overrides the "PASSWORD_STORE_DIR" environment variable
|
||||
PasswordDirectory figtree.StringOption `yaml:"password-directory,omitempty" json:"password-directory,omitempty"`
|
||||
|
||||
// PasswordName is the the name of the password key entry stored used with PasswordSource `pass`.
|
||||
PasswordName figtree.StringOption `yaml:"password-name,omitempty" json:"password-name,omitempty"`
|
||||
|
||||
// Quiet will lower the defalt log level to suppress the standard output for commands
|
||||
Quiet figtree.BoolOption `yaml:"quiet,omitempty" json:"quiet,omitempty"`
|
||||
|
||||
// SocksProxy is used to configure the http client to access the Endpoint via a socks proxy. The value
|
||||
// should be a ip address and port string, something like "127.0.0.1:1080"
|
||||
SocksProxy figtree.StringOption `yaml:"socksproxy,omitempty" json:"socksproxy,omitempty"`
|
||||
|
||||
// UnixProxy is use to configure the http client to access the Endpoint via a local unix domain socket used
|
||||
// to proxy requests
|
||||
UnixProxy figtree.StringOption `yaml:"unixproxy,omitempty" json:"unixproxy,omitempty"`
|
||||
|
||||
// User is use to represent the user on the Jira service. This can be different from the username used to
|
||||
// authenticate with the service. For example when using AuthenticationMethod `api-token` the Login is
|
||||
// typically an email address like `username@example.com` and the User property would be someting like
|
||||
// `username` The User property is used on Jira service API calls that require a user to associate with
|
||||
// an Issue (like assigning a Issue to yourself)
|
||||
User figtree.StringOption `yaml:"user,omitempty" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type CommonOptions struct {
|
||||
@@ -65,54 +124,71 @@ type kingpinAppOrCommand interface {
|
||||
GetCommand(string) *kingpin.CmdClause
|
||||
}
|
||||
|
||||
func Register(app *kingpin.Application, o *oreo.Client, fig *figtree.FigTree, reg []CommandRegistry) {
|
||||
var globalCommandRegistry = []CommandRegistry{}
|
||||
|
||||
func RegisterCommand(regEntry CommandRegistry) {
|
||||
globalCommandRegistry = append(globalCommandRegistry, regEntry)
|
||||
}
|
||||
|
||||
func (o *GlobalOptions) AuthMethod() string {
|
||||
if strings.Contains(o.Endpoint.Value, ".atlassian.net") && o.AuthenticationMethod.Source == "default" {
|
||||
return "api-token"
|
||||
}
|
||||
return o.AuthenticationMethod.Value
|
||||
}
|
||||
|
||||
func register(app *kingpin.Application, o *oreo.Client, fig *figtree.FigTree) {
|
||||
globals := GlobalOptions{
|
||||
User: figtree.NewStringOption(os.Getenv("USER")),
|
||||
User: figtree.NewStringOption(os.Getenv("USER")),
|
||||
AuthenticationMethod: figtree.NewStringOption("session"),
|
||||
}
|
||||
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.Flag("socksproxy", "Address for a socks proxy").SetValue(&globals.SocksProxy)
|
||||
app.Flag("user", "user name used within the Jira service").Short('u').SetValue(&globals.User)
|
||||
app.Flag("login", "login name that corresponds to the user used for authentication").SetValue(&globals.Login)
|
||||
|
||||
app.PreAction(func(_ *kingpin.ParseContext) error {
|
||||
if globals.Insecure.Value {
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
o = o.WithPreCallback(
|
||||
func(req *http.Request) (*http.Request, error) {
|
||||
if globals.AuthMethod() == "api-token" {
|
||||
// need to set basic auth header with user@domain:api-token
|
||||
token := globals.GetPass()
|
||||
authHeader := fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", globals.Login.Value, token))))
|
||||
req.Header.Add("Authorization", authHeader)
|
||||
}
|
||||
o = o.WithTransport(transport)
|
||||
}
|
||||
if globals.UnixProxy.Value != "" {
|
||||
o = o.WithTransport(unixProxy(globals.UnixProxy.Value))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return req, 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
|
||||
if globals.AuthMethod() == "session" {
|
||||
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"})
|
||||
// we are not logged in, so force login now by running the "login" command
|
||||
app.Parse([]string{"login"})
|
||||
|
||||
// rerun the original request
|
||||
// rerun the original request
|
||||
return o.Do(req)
|
||||
}
|
||||
} else if globals.AuthMethod() == "api-token" && resp.StatusCode == 401 {
|
||||
globals.SetPass("")
|
||||
return o.Do(req)
|
||||
}
|
||||
return resp, nil
|
||||
},
|
||||
)
|
||||
|
||||
for _, command := range reg {
|
||||
for _, command := range globalCommandRegistry {
|
||||
copy := command
|
||||
commandFields := strings.Fields(copy.Command)
|
||||
var appOrCmd kingpinAppOrCommand = app
|
||||
@@ -128,6 +204,29 @@ func Register(app *kingpin.Application, o *oreo.Client, fig *figtree.FigTree, re
|
||||
|
||||
cmd := appOrCmd.Command(commandFields[len(commandFields)-1], copy.Entry.Help)
|
||||
LoadConfigs(cmd, fig, &globals)
|
||||
cmd.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))
|
||||
} else if globals.SocksProxy.Value != "" {
|
||||
o = o.WithTransport(socksProxy(globals.SocksProxy.Value))
|
||||
}
|
||||
if globals.AuthMethod() == "api-token" {
|
||||
o = o.WithCookieFile("")
|
||||
}
|
||||
if globals.Login.Value == "" {
|
||||
globals.Login = globals.User
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, alias := range copy.Aliases {
|
||||
cmd = cmd.Alias(alias)
|
||||
@@ -141,6 +240,9 @@ func Register(app *kingpin.Application, o *oreo.Client, fig *figtree.FigTree, re
|
||||
|
||||
cmd.Action(
|
||||
func(_ *kingpin.ParseContext) error {
|
||||
if logging.GetLevel("") > logging.DEBUG {
|
||||
o = o.WithTrace(true)
|
||||
}
|
||||
return copy.Entry.ExecuteFunc(o, &globals)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package jiracli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logging.MustGetLogger("jira")
|
||||
)
|
||||
|
||||
func IncreaseLogLevel(verbosity int) {
|
||||
logging.SetLevel(logging.GetLevel("")+logging.Level(verbosity), "")
|
||||
}
|
||||
|
||||
func InitLogging() {
|
||||
logBackend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||
format := os.Getenv("JIRA_LOG_FORMAT")
|
||||
if format == "" {
|
||||
format = "%{color}%{level:-5s}%{color:reset} %{message}"
|
||||
}
|
||||
logging.SetBackend(
|
||||
logging.NewBackendFormatter(
|
||||
logBackend,
|
||||
logging.MustStringFormatter(format),
|
||||
),
|
||||
)
|
||||
if os.Getenv("JIRA_DEBUG") == "" {
|
||||
logging.SetLevel(logging.NOTICE, "")
|
||||
} else {
|
||||
logging.SetLevel(logging.DEBUG, "")
|
||||
if verbosity, err := strconv.Atoi(os.Getenv("JIRA_DEBUG")); err == nil {
|
||||
IncreaseLogLevel(verbosity)
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-6
@@ -3,6 +3,7 @@ package jiracli
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
@@ -12,24 +13,44 @@ import (
|
||||
|
||||
func (o *GlobalOptions) ProvideAuthParams() *jiradata.AuthParams {
|
||||
return &jiradata.AuthParams{
|
||||
Username: o.User.Value,
|
||||
Username: o.Login.Value,
|
||||
Password: o.GetPass(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *GlobalOptions) keyName() string {
|
||||
user := o.Login.Value
|
||||
if o.AuthMethod() == "api-token" {
|
||||
user = "api-token:" + user
|
||||
}
|
||||
|
||||
if o.PasswordSource.Value == "pass" {
|
||||
if o.PasswordName.Value != "" {
|
||||
return o.PasswordName.Value
|
||||
}
|
||||
return fmt.Sprintf("GoJira/%s", user)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (o *GlobalOptions) GetPass() string {
|
||||
passwd := ""
|
||||
if o.PasswordSource.Value != "" {
|
||||
if o.PasswordSource.Value == "keyring" {
|
||||
var err error
|
||||
passwd, err = keyringGet(o.User.Value)
|
||||
passwd, err = keyringGet(o.keyName())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else if o.PasswordSource.Value == "pass" {
|
||||
if o.PasswordDirectory.Value != "" {
|
||||
orig := os.Getenv("PASSWORD_STORE_DIR")
|
||||
os.Setenv("PASSWORD_STORE_DIR", o.PasswordDirectory.Value)
|
||||
defer os.Setenv("PASSWORD_STORE_DIR", orig)
|
||||
}
|
||||
if bin, err := exec.LookPath("pass"); err == nil {
|
||||
buf := bytes.NewBufferString("")
|
||||
cmd := exec.Command(bin, fmt.Sprintf("GoJira/%s", o.User))
|
||||
cmd := exec.Command(bin, o.keyName())
|
||||
cmd.Stdout = buf
|
||||
cmd.Stderr = buf
|
||||
if err := cmd.Run(); err == nil {
|
||||
@@ -44,9 +65,23 @@ func (o *GlobalOptions) GetPass() string {
|
||||
if passwd != "" {
|
||||
return passwd
|
||||
}
|
||||
|
||||
if passwd = os.Getenv("JIRA_API_TOKEN"); passwd != "" && o.AuthMethod() == "api-token" {
|
||||
return passwd
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf("Jira Password [%s]: ", o.Login)
|
||||
help := ""
|
||||
|
||||
if o.AuthMethod() == "api-token" {
|
||||
prompt = fmt.Sprintf("Jira API-Token [%s]: ", o.Login)
|
||||
help = "API Tokens may be required by your Jira service endpoint: https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-basic-auth-and-cookie-based-auth/"
|
||||
}
|
||||
|
||||
err := survey.AskOne(
|
||||
&survey.Password{
|
||||
Message: fmt.Sprintf("Jira Password [%s]: ", o.User),
|
||||
Message: prompt,
|
||||
Help: help,
|
||||
},
|
||||
&passwd,
|
||||
nil,
|
||||
@@ -62,15 +97,20 @@ func (o *GlobalOptions) GetPass() string {
|
||||
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)
|
||||
err := keyringSet(o.keyName(), passwd)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to set password in keyring: %s", err)
|
||||
return err
|
||||
}
|
||||
} else if o.PasswordSource.Value == "pass" {
|
||||
if o.PasswordDirectory.Value != "" {
|
||||
orig := os.Getenv("PASSWORD_STORE_DIR")
|
||||
os.Setenv("PASSWORD_STORE_DIR", o.PasswordDirectory.Value)
|
||||
defer os.Setenv("PASSWORD_STORE_DIR", orig)
|
||||
}
|
||||
if bin, err := exec.LookPath("pass"); err == nil {
|
||||
log.Debugf("using %s", bin)
|
||||
passName := fmt.Sprintf("GoJira/%s", o.User)
|
||||
passName := o.keyName()
|
||||
if passwd != "" {
|
||||
in := bytes.NewBufferString(fmt.Sprintf("%s\n%s\n", passwd, passwd))
|
||||
out := bytes.NewBufferString("")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package jiracli
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
func socksProxy(address string) *http.Transport {
|
||||
return newSocksProxyTransport(address)
|
||||
}
|
||||
|
||||
func newSocksProxyTransport(address string) *http.Transport {
|
||||
dialer, err := proxy.SOCKS5("tcp", address, nil, proxy.Direct)
|
||||
if err != nil {
|
||||
// TODO: whoops, return error?
|
||||
panic(err)
|
||||
}
|
||||
dial := func(network, addr string) (net.Conn, error) {
|
||||
return dialer.Dial(network, addr)
|
||||
}
|
||||
|
||||
return &http.Transport{
|
||||
Dial: dial,
|
||||
DisableKeepAlives: true,
|
||||
ResponseHeaderTimeout: 30 * time.Second,
|
||||
ExpectContinueTimeout: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
+27
-3
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
yaml "gopkg.in/coryb/yaml.v2"
|
||||
|
||||
"github.com/coryb/figtree"
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/mgutz/ansi"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
@@ -65,6 +67,24 @@ func TemplateProcessor() *template.Template {
|
||||
"jira": func() string {
|
||||
return os.Args[0]
|
||||
},
|
||||
"env": func() map[string]string {
|
||||
out := map[string]string{}
|
||||
for _, env := range os.Environ() {
|
||||
kv := strings.SplitN(env, "=", 2)
|
||||
out[kv[0]] = kv[1]
|
||||
}
|
||||
return out
|
||||
},
|
||||
"shellquote": func(content string) string {
|
||||
return shellquote.Join(content)
|
||||
},
|
||||
"toMinJson": func(content interface{}) (string, error) {
|
||||
bytes, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
},
|
||||
"toJson": func(content interface{}) (string, error) {
|
||||
bytes, err := json.MarshalIndent(content, "", " ")
|
||||
if err != nil {
|
||||
@@ -123,6 +143,10 @@ func TemplateProcessor() *template.Template {
|
||||
"color": func(color string) string {
|
||||
return ansi.ColorCode(color)
|
||||
},
|
||||
"regReplace": func(search string, replace string, content string) string {
|
||||
re := regexp.MustCompile(search)
|
||||
return re.ReplaceAllString(content, replace)
|
||||
},
|
||||
"split": func(sep string, content string) []string {
|
||||
return strings.Split(content, sep)
|
||||
},
|
||||
@@ -134,7 +158,7 @@ func TemplateProcessor() *template.Template {
|
||||
return strings.Join(vals, sep)
|
||||
},
|
||||
"abbrev": func(max int, content string) string {
|
||||
if len(content) > max {
|
||||
if len(content) > max && max > 2 {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(content[:max-3])
|
||||
buffer.WriteString("...")
|
||||
@@ -266,7 +290,7 @@ const defaultTableTemplate = `{{/* table template */ -}}
|
||||
| {{ "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}} |
|
||||
| {{ .key | printf "%-14s"}} | {{ .fields.summary | abbrev (sub $w 2) | printf (printf "%%-%ds" (sub $w 2)) }} | {{.fields.issuetype.name | printf "%-12s" }} | {{if .fields.priority}}{{.fields.priority.name | printf "%-12s" }}{{else}}<unassigned>{{end}} | {{.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 }}+
|
||||
`
|
||||
@@ -480,7 +504,7 @@ fields:
|
||||
{{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: {{.}}{{end}}{{else}}{{range .fields.fixVersions}}
|
||||
- name: {{.name}}{{end}}{{end}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
package jiracli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"github.com/coryb/figtree"
|
||||
"github.com/coryb/kingpeon"
|
||||
"github.com/coryb/oreo"
|
||||
jira "gopkg.in/Netflix-Skunkworks/go-jira.v1"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
var usage = `{{define "FormatCommand"}}\
|
||||
{{if .FlagSummary}} {{.FlagSummary}}{{end}}\
|
||||
{{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\
|
||||
{{end}}\
|
||||
|
||||
{{define "FormatBriefCommands"}}\
|
||||
{{range .FlattenedCommands}}\
|
||||
{{if not .Hidden}}\
|
||||
{{ print .FullCommand ":" | printf "%-20s"}} {{.Help}}
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
|
||||
{{define "FormatCommands"}}\
|
||||
{{range .FlattenedCommands}}\
|
||||
{{if not .Hidden}}\
|
||||
{{.FullCommand}}{{if .Default}}*{{end}}{{template "FormatCommand" .}}
|
||||
{{.Help|Wrap 4}}
|
||||
{{with .Flags|FlagsToTwoColumns}}{{FormatTwoColumnsWithIndent . 4 2}}{{end}}
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
|
||||
{{define "FormatUsage"}}\
|
||||
{{template "FormatCommand" .}}{{if .Commands}} <command> [<args> ...]{{end}}
|
||||
{{if .Help}}
|
||||
{{.Help|Wrap 0}}\
|
||||
{{end}}\
|
||||
|
||||
{{end}}\
|
||||
|
||||
{{if .Context.SelectedCommand}}\
|
||||
usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatCommand" .Context.SelectedCommand}}
|
||||
{{if .Context.SelectedCommand.Aliases }}\
|
||||
{{range $top := .App.Commands}}\
|
||||
{{if eq $top.FullCommand $.Context.SelectedCommand.FullCommand}}\
|
||||
{{range $alias := $.Context.SelectedCommand.Aliases}}\
|
||||
alias: {{$.App.Name}} {{$alias}}{{template "FormatCommand" $.Context.SelectedCommand}}
|
||||
{{end}}\
|
||||
{{else}}\
|
||||
{{range $sub := $top.Commands}}\
|
||||
{{if eq $sub.FullCommand $.Context.SelectedCommand.FullCommand}}\
|
||||
{{range $alias := $.Context.SelectedCommand.Aliases}}\
|
||||
alias: {{$.App.Name}} {{$top.Name}} {{$alias}}{{template "FormatCommand" $.Context.SelectedCommand}}
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{end}}
|
||||
{{if .Context.SelectedCommand.Help}}\
|
||||
{{.Context.SelectedCommand.Help|Wrap 0}}
|
||||
{{end}}\
|
||||
{{else}}\
|
||||
usage: {{.App.Name}}{{template "FormatUsage" .App}}
|
||||
{{end}}\
|
||||
|
||||
{{if .App.Flags}}\
|
||||
Global flags:
|
||||
{{.App.Flags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end}}\
|
||||
{{if .Context.SelectedCommand}}\
|
||||
{{if and .Context.SelectedCommand.Flags|RequiredFlags}}\
|
||||
Required flags:
|
||||
{{.Context.SelectedCommand.Flags|RequiredFlags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end}}\
|
||||
{{if .Context.SelectedCommand.Flags|OptionalFlags}}\
|
||||
Optional flags:
|
||||
{{.Context.SelectedCommand.Flags|OptionalFlags|FlagsToTwoColumns|FormatTwoColumns}}
|
||||
{{end}}\
|
||||
{{end}}\
|
||||
{{if .Context.Args}}\
|
||||
Args:
|
||||
{{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}}
|
||||
{{end}}\
|
||||
{{if .Context.SelectedCommand}}\
|
||||
{{if .Context.SelectedCommand.Commands}}\
|
||||
Subcommands:
|
||||
{{template "FormatCommands" .Context.SelectedCommand}}
|
||||
{{end}}\
|
||||
{{else if .App.Commands}}\
|
||||
Commands:
|
||||
{{template "FormatBriefCommands" .App}}
|
||||
{{end}}\
|
||||
`
|
||||
|
||||
func CommandLine(fig *figtree.FigTree, o *oreo.Client) *kingpin.Application {
|
||||
app := kingpin.New("jira", "Jira Command Line Interface")
|
||||
app.UsageWriter(os.Stdout)
|
||||
app.ErrorWriter(os.Stderr)
|
||||
app.Command("version", "Prints version").PreAction(func(*kingpin.ParseContext) error {
|
||||
fmt.Println(jira.VERSION)
|
||||
panic(Exit{Code: 0})
|
||||
})
|
||||
app.UsageTemplate(usage)
|
||||
|
||||
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)
|
||||
|
||||
app.Terminate(func(status int) {
|
||||
for _, arg := range os.Args {
|
||||
if arg == "-h" || arg == "--help" || len(os.Args) == 1 {
|
||||
panic(Exit{Code: 0})
|
||||
}
|
||||
}
|
||||
panic(Exit{Code: 1})
|
||||
})
|
||||
|
||||
register(app, o, fig)
|
||||
|
||||
// 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(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, TemplateProcessor())
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func ParseCommandLine(app *kingpin.Application, args []string) {
|
||||
// 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(args)
|
||||
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.(*Error); ok {
|
||||
log.Errorf("%s", err)
|
||||
panic(Exit{Code: 1})
|
||||
} else {
|
||||
ctx, _ := app.ParseContext(os.Args[1:])
|
||||
if ctx != nil {
|
||||
app.UsageForContext(ctx)
|
||||
}
|
||||
log.Errorf("Invalid Usage: %s", err)
|
||||
panic(Exit{Code: 1})
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
@@ -22,7 +23,11 @@ func Homedir() string {
|
||||
}
|
||||
|
||||
func findClosestParentPath(fileName string) (string, error) {
|
||||
paths := figtree.FindParentPaths(fileName)
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
paths := figtree.FindParentPaths(Homedir(), cwd, fileName)
|
||||
if len(paths) > 0 {
|
||||
return paths[len(paths)-1], nil
|
||||
}
|
||||
@@ -30,7 +35,7 @@ func findClosestParentPath(fileName string) (string, error) {
|
||||
}
|
||||
|
||||
func tmpYml(tmpFilePrefix string) (*os.File, error) {
|
||||
fh, err := ioutil.TempFile("", tmpFilePrefix)
|
||||
fh, err := ioutil.TempFile("", filepath.Base(tmpFilePrefix))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ func CmdAssign(o *oreo.Client, globals *jiracli.GlobalOptions, opts *AssignOptio
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
+2
-2
@@ -66,8 +66,8 @@ func CmdBlock(o *oreo.Client, globals *jiracli.GlobalOptions, opts *BlockOptions
|
||||
}
|
||||
|
||||
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)
|
||||
fmt.Printf("OK %s %s\n", opts.InwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.InwardIssue.Key))
|
||||
fmt.Printf("OK %s %s\n", opts.OutwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.OutwardIssue.Key))
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
+2
-3
@@ -1,11 +1,10 @@
|
||||
package jiracmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/coryb/figtree"
|
||||
"github.com/coryb/oreo"
|
||||
"github.com/pkg/browser"
|
||||
jira "gopkg.in/Netflix-Skunkworks/go-jira.v1"
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
@@ -27,5 +26,5 @@ func CmdBrowseRegistry() *jiracli.CommandRegistryEntry {
|
||||
|
||||
// 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))
|
||||
return browser.OpenURL(jira.URLJoin(globals.Endpoint.Value, "browse", issue))
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ func CmdComment(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CommentOpt
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
+3
-2
@@ -93,8 +93,9 @@ func CmdCreate(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CreateOptio
|
||||
return err
|
||||
}
|
||||
|
||||
browseLink := jira.URLJoin(globals.Endpoint.Value, "browse", issueResp.Key)
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueResp.Key, globals.Endpoint.Value, issueResp.Key)
|
||||
fmt.Printf("OK %s %s\n", issueResp.Key, browseLink)
|
||||
}
|
||||
|
||||
if opts.SaveFile != "" {
|
||||
@@ -105,7 +106,7 @@ func CmdCreate(o *oreo.Client, globals *jiracli.GlobalOptions, opts *CreateOptio
|
||||
defer fh.Close()
|
||||
out, err := yaml.Marshal(map[string]string{
|
||||
"issue": issueResp.Key,
|
||||
"link": fmt.Sprintf("%s/browse/%s", globals.Endpoint.Value, issueResp.Key),
|
||||
"link": browseLink,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+2
-2
@@ -67,7 +67,7 @@ func CmdDup(o *oreo.Client, globals *jiracli.GlobalOptions, opts *DupOptions) er
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.OutwardIssue.Key, globals.Endpoint.Value, opts.OutwardIssue.Key)
|
||||
fmt.Printf("OK %s %s\n", opts.OutwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.OutwardIssue.Key))
|
||||
}
|
||||
|
||||
meta, err := jira.GetIssueTransitions(o, globals.Endpoint.Value, opts.InwardIssue.Key)
|
||||
@@ -103,7 +103,7 @@ func CmdDup(o *oreo.Client, globals *jiracli.GlobalOptions, opts *DupOptions) er
|
||||
}
|
||||
|
||||
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\n", opts.InwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.InwardIssue.Key))
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
+5
-5
@@ -33,12 +33,12 @@ func CmdEditRegistry() *jiracli.CommandRegistryEntry {
|
||||
"Edit issue details",
|
||||
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
|
||||
jiracli.LoadConfigs(cmd, fig, &opts)
|
||||
if opts.QueryFields == "" {
|
||||
opts.QueryFields = "assignee,created,priority,reporter,status,summary,updated,issuetype,comments,description,votes,created,customfield_10110,components"
|
||||
}
|
||||
return CmdEditUsage(cmd, &opts, fig)
|
||||
},
|
||||
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
|
||||
if opts.QueryFields == "" {
|
||||
opts.QueryFields = "assignee,created,priority,reporter,status,summary,updated,issuetype,comment,description,votes,created,customfield_10110,components"
|
||||
}
|
||||
return CmdEdit(o, globals, &opts)
|
||||
},
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func CmdEdit(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EditOptions)
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
@@ -145,7 +145,7 @@ func CmdEdit(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EditOptions)
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueData.Key, globals.Endpoint.Value, issueData.Key)
|
||||
fmt.Printf("OK %s %s\n", issueData.Key, jira.URLJoin(globals.Endpoint.Value, "browse", issueData.Key))
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, issueData.Key)
|
||||
|
||||
+2
-2
@@ -44,9 +44,9 @@ func CmdEpicAdd(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EpicAddOpt
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Epic, globals.Endpoint.Value, opts.Epic)
|
||||
fmt.Printf("OK %s %s\n", opts.Epic, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Epic))
|
||||
for _, issue := range opts.Issues {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, globals.Endpoint.Value, issue)
|
||||
fmt.Printf("OK %s %s\n", issue, jira.URLJoin(globals.Endpoint.Value, "browse", issue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -26,6 +26,9 @@ func CmdEpicListRegistry() *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)
|
||||
return CmdEpicListUsage(cmd, &opts, fig)
|
||||
},
|
||||
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
|
||||
if opts.MaxResults == 0 {
|
||||
opts.MaxResults = 500
|
||||
}
|
||||
@@ -35,9 +38,6 @@ func CmdEpicListRegistry() *jiracli.CommandRegistryEntry {
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func CmdEpicRemove(o *oreo.Client, globals *jiracli.GlobalOptions, opts *EpicRem
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
for _, issue := range opts.Issues {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issue, globals.Endpoint.Value, issue)
|
||||
fmt.Printf("OK %s %s\n", issue, jira.URLJoin(globals.Endpoint.Value, "browse", issue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ func CmdExportTemplatesRegistry() *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 {
|
||||
if opts.Dir == "" {
|
||||
opts.Dir = fmt.Sprintf("%s/.jira.d/templates", jiracli.Homedir())
|
||||
}
|
||||
return CmdExportTemplates(globals, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ func CmdIssueLink(o *oreo.Client, globals *jiracli.GlobalOptions, opts *IssueLin
|
||||
}
|
||||
|
||||
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)
|
||||
fmt.Printf("OK %s %s\n", opts.InwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.InwardIssue.Key))
|
||||
fmt.Printf("OK %s %s\n", opts.OutwardIssue.Key, jira.URLJoin(globals.Endpoint.Value, "browse", opts.OutwardIssue.Key))
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
@@ -57,7 +57,7 @@ func CmdLabelsAdd(o *oreo.Client, globals *jiracli.GlobalOptions, opts *LabelsAd
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
|
||||
@@ -58,7 +58,7 @@ func CmdLabelsRemove(o *oreo.Client, globals *jiracli.GlobalOptions, opts *Label
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
|
||||
@@ -55,7 +55,7 @@ func CmdLabelsSet(o *oreo.Client, globals *jiracli.GlobalOptions, opts *LabelsSe
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
|
||||
+3
-3
@@ -27,6 +27,9 @@ func CmdListRegistry() *jiracli.CommandRegistryEntry {
|
||||
"Prints list of issues for given search criteria",
|
||||
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
|
||||
jiracli.LoadConfigs(cmd, fig, &opts)
|
||||
return CmdListUsage(cmd, &opts, fig)
|
||||
},
|
||||
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
|
||||
if opts.MaxResults == 0 {
|
||||
opts.MaxResults = 500
|
||||
}
|
||||
@@ -36,9 +39,6 @@ func CmdListRegistry() *jiracli.CommandRegistryEntry {
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,6 +46,11 @@ func authCallback(req *http.Request, resp *http.Response) (*http.Response, error
|
||||
|
||||
// CmdLogin will attempt to login into jira server
|
||||
func CmdLogin(o *oreo.Client, globals *jiracli.GlobalOptions, opts *jiracli.CommonOptions) error {
|
||||
if globals.AuthMethod() == "api-token" {
|
||||
log.Noticef("No need to login when using api-token authentication method")
|
||||
return nil
|
||||
}
|
||||
|
||||
ua := o.WithoutRedirect().WithRetries(0).WithoutCallbacks().WithPostCallback(authCallback)
|
||||
for {
|
||||
if session, err := jira.GetSession(o, globals.Endpoint.Value); err != nil {
|
||||
|
||||
+26
-1
@@ -2,10 +2,13 @@ package jiracmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/coryb/figtree"
|
||||
"github.com/coryb/oreo"
|
||||
"github.com/mgutz/ansi"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
survey "gopkg.in/AlecAivazis/survey.v1"
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1"
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
@@ -14,7 +17,7 @@ import (
|
||||
func CmdLogoutRegistry() *jiracli.CommandRegistryEntry {
|
||||
opts := jiracli.CommonOptions{}
|
||||
return &jiracli.CommandRegistryEntry{
|
||||
"Deactivate sesssion with Jira server",
|
||||
"Deactivate session with Jira server",
|
||||
func(fig *figtree.FigTree, cmd *kingpin.CmdClause) error {
|
||||
jiracli.LoadConfigs(cmd, fig, &opts)
|
||||
return nil
|
||||
@@ -27,6 +30,28 @@ func CmdLogoutRegistry() *jiracli.CommandRegistryEntry {
|
||||
|
||||
// CmdLogout will attempt to terminate an active Jira session
|
||||
func CmdLogout(o *oreo.Client, globals *jiracli.GlobalOptions, opts *jiracli.CommonOptions) error {
|
||||
if globals.AuthMethod() == "api-token" {
|
||||
log.Noticef("No need to logout when using api-token authentication method")
|
||||
if globals.GetPass() != "" && terminal.IsTerminal(int(os.Stdin.Fd())) && terminal.IsTerminal(int(os.Stdout.Fd())) {
|
||||
delete := false
|
||||
err := survey.AskOne(
|
||||
&survey.Confirm{
|
||||
Message: fmt.Sprintf("Delete api-token from password provider [%s]: ", globals.PasswordSource),
|
||||
Default: false,
|
||||
},
|
||||
&delete,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("%s", err)
|
||||
panic(jiracli.Exit{Code: 1})
|
||||
}
|
||||
if delete {
|
||||
globals.SetPass("")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ua := o.WithoutRedirect().WithRetries(0).WithoutCallbacks()
|
||||
err := jira.DeleteSession(ua, globals.Endpoint.Value)
|
||||
if err == nil {
|
||||
|
||||
+2
-2
@@ -59,8 +59,8 @@ func CmdRank(o *oreo.Client, globals *jiracli.GlobalOptions, opts *RankOptions)
|
||||
}
|
||||
|
||||
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)
|
||||
fmt.Printf("OK %s %s\n", opts.First, jira.URLJoin(globals.Endpoint.Value, "browse", opts.First))
|
||||
fmt.Printf("OK %s %s\n", opts.Second, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Second))
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package jiracmd
|
||||
|
||||
import "gopkg.in/Netflix-Skunkworks/go-jira.v1/jiracli"
|
||||
|
||||
func RegisterAllCommands() {
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "acknowledge", Entry: CmdTransitionRegistry("acknowledge"), Aliases: []string{"ack"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "assign", Entry: CmdAssignRegistry(), Aliases: []string{"give"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "attach create", Entry: CmdAttachCreateRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "attach get", Entry: CmdAttachGetRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "attach list", Entry: CmdAttachListRegistry(), Aliases: []string{"ls"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "attach remove", Entry: CmdAttachRemoveRegistry(), Aliases: []string{"rm"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "backlog", Entry: CmdTransitionRegistry("Backlog")})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "block", Entry: CmdBlockRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "browse", Entry: CmdBrowseRegistry(), Aliases: []string{"b"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "close", Entry: CmdTransitionRegistry("close")})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "comment", Entry: CmdCommentRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "component add", Entry: CmdComponentAddRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "components", Entry: CmdComponentsRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "create", Entry: CmdCreateRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "createmeta", Entry: CmdCreateMetaRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "done", Entry: CmdTransitionRegistry("Done")})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "dup", Entry: CmdDupRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "edit", Entry: CmdEditRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "editmeta", Entry: CmdEditMetaRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "epic add", Entry: CmdEpicAddRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "epic create", Entry: CmdEpicCreateRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "epic list", Entry: CmdEpicListRegistry(), Aliases: []string{"ls"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "epic remove", Entry: CmdEpicRemoveRegistry(), Aliases: []string{"rm"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "export-templates", Entry: CmdExportTemplatesRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "fields", Entry: CmdFieldsRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "in-progress", Entry: CmdTransitionRegistry("Progress"), Aliases: []string{"prog", "progress"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "issuelink", Entry: CmdIssueLinkRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "issuelinktypes", Entry: CmdIssueLinkTypesRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "issuetypes", Entry: CmdIssueTypesRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "labels add", Entry: CmdLabelsAddRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "labels remove", Entry: CmdLabelsRemoveRegistry(), Aliases: []string{"rm"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "labels set", Entry: CmdLabelsSetRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "list", Entry: CmdListRegistry(), Aliases: []string{"ls"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "login", Entry: CmdLoginRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "logout", Entry: CmdLogoutRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "rank", Entry: CmdRankRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "reopen", Entry: CmdTransitionRegistry("reopen")})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "request", Entry: CmdRequestRegistry(), Aliases: []string{"req"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "resolve", Entry: CmdTransitionRegistry("resolve")})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "start", Entry: CmdTransitionRegistry("start")})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "stop", Entry: CmdTransitionRegistry("stop")})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "subtask", Entry: CmdSubtaskRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "take", Entry: CmdTakeRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "todo", Entry: CmdTransitionRegistry("To Do")})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "transition", Entry: CmdTransitionRegistry(""), Aliases: []string{"trans"}})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "transitions", Entry: CmdTransitionsRegistry("transitions")})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "transmeta", Entry: CmdTransitionsRegistry("debug")})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "unassign", Entry: CmdUnassignRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "unexport-templates", Entry: CmdUnexportTemplatesRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "view", Entry: CmdViewRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "vote", Entry: CmdVoteRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "watch", Entry: CmdWatchRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "worklog add", Entry: CmdWorklogAddRegistry()})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "worklog list", Entry: CmdWorklogListRegistry(), Default: true})
|
||||
jiracli.RegisterCommand(jiracli.CommandRegistry{Command: "session", Entry: CmdSessionRegistry()})
|
||||
}
|
||||
+3
-3
@@ -32,14 +32,14 @@ func CmdRequestRegistry() *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 {
|
||||
if opts.Method == "" {
|
||||
opts.Method = "GET"
|
||||
}
|
||||
return CmdRequest(o, globals, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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"
|
||||
yaml "gopkg.in/coryb/yaml.v2"
|
||||
)
|
||||
|
||||
func CmdSessionRegistry() *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 CmdSession(o, globals, &opts)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CmdSession will attempt to login into jira server
|
||||
func CmdSession(o *oreo.Client, globals *jiracli.GlobalOptions, opts *jiracli.CommonOptions) error {
|
||||
ua := o.WithoutRedirect().WithRetries(0).WithoutPostCallbacks()
|
||||
session, err := jira.GetSession(ua, globals.Endpoint.Value)
|
||||
var output []byte
|
||||
if err != nil {
|
||||
defer panic(jiracli.Exit{1})
|
||||
output, err = yaml.Marshal(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
output, err = yaml.Marshal(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Print(string(output))
|
||||
return nil
|
||||
}
|
||||
+4
-4
@@ -33,12 +33,12 @@ func CmdSubtaskRegistry() *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 {
|
||||
if opts.IssueType == "" {
|
||||
opts.IssueType = "Sub-task"
|
||||
}
|
||||
return CmdSubtask(o, globals, &opts)
|
||||
},
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func CmdSubtask(o *oreo.Client, globals *jiracli.GlobalOptions, opts *SubtaskOpt
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueResp.Key, globals.Endpoint.Value, issueResp.Key)
|
||||
fmt.Printf("OK %s %s\n", issueResp.Key, jira.URLJoin(globals.Endpoint.Value, "browse", issueResp.Key))
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
+3
-1
@@ -17,7 +17,9 @@ func CmdTakeRegistry() *jiracli.CommandRegistryEntry {
|
||||
return CmdAssignUsage(cmd, &opts)
|
||||
},
|
||||
func(o *oreo.Client, globals *jiracli.GlobalOptions) error {
|
||||
opts.Assignee = globals.User.Value
|
||||
if opts.Assignee == "" {
|
||||
opts.Assignee = globals.User.Value
|
||||
}
|
||||
return CmdAssign(o, globals, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ func CmdTransitionUsage(cmd *kingpin.CmdClause, opts *TransitionOptions) error {
|
||||
cmd.Arg("TRANSITION", "State to transition issue to").Required().StringVar(&opts.Transition)
|
||||
}
|
||||
cmd.Arg("ISSUE", "issue to transition").Required().StringVar(&opts.Issue)
|
||||
cmd.Flag("resolution", "Set resolution on transition").StringVar(&opts.Resolution)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -156,7 +157,7 @@ func CmdTransition(o *oreo.Client, globals *jiracli.GlobalOptions, opts *Transit
|
||||
return jiracli.CliError(err)
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", issueData.Key, globals.Endpoint.Value, issueData.Key)
|
||||
fmt.Printf("OK %s %s\n", issueData.Key, jira.URLJoin(globals.Endpoint.Value, "browse", issueData.Key))
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
@@ -20,13 +20,12 @@ func CmdUnexportTemplatesRegistry() *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 {
|
||||
if opts.Dir == "" {
|
||||
opts.Dir = fmt.Sprintf("%s/.jira.d/templates", jiracli.Homedir())
|
||||
}
|
||||
return CmdUnexportTemplates(globals, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ func CmdVote(o *oreo.Client, globals *jiracli.GlobalOptions, opts *VoteOptions)
|
||||
}
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ func CmdWatch(o *oreo.Client, globals *jiracli.GlobalOptions, opts *WatchOptions
|
||||
}
|
||||
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
}
|
||||
|
||||
if opts.Browse.Value {
|
||||
|
||||
@@ -59,7 +59,7 @@ func CmdWorklogAdd(o *oreo.Client, globals *jiracli.GlobalOptions, opts *Worklog
|
||||
return err
|
||||
}
|
||||
if !globals.Quiet.Value {
|
||||
fmt.Printf("OK %s %s/browse/%s\n", opts.Issue, globals.Endpoint.Value, opts.Issue)
|
||||
fmt.Printf("OK %s %s\n", opts.Issue, jira.URLJoin(globals.Endpoint.Value, "browse", opts.Issue))
|
||||
}
|
||||
if opts.Browse.Value {
|
||||
return CmdBrowse(globals, opts.Issue)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package jiradata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAttachmentID(t *testing.T) {
|
||||
// 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 to include:
|
||||
// ID IntOrString `json:"id,omitempty" yaml:"id,omitempty"`
|
||||
assert.IsType(t, IntOrString(0), Attachment{}.ID)
|
||||
}
|
||||
@@ -46,11 +46,12 @@ package jiradata
|
||||
// }
|
||||
// }
|
||||
type IssueType struct {
|
||||
AvatarID int `json:"avatarId,omitempty" yaml:"avatarId,omitempty"`
|
||||
Description string `json:"description,omitempty" yaml:"description,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"`
|
||||
AvatarID int `json:"avatarId,omitempty" yaml:"avatarId,omitempty"`
|
||||
Description string `json:"description,omitempty" yaml:"description,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"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package jiradata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIssueTypeFields(t *testing.T) {
|
||||
// this is because schema is wrong, missing the 'Fields' arguments, so we manually add it.
|
||||
// If the jiradata is regenerated we need to manually make the change again to include:
|
||||
// Fields FieldMetaMap `json:"fields,omitempty" yaml:"fields,omitempty"`
|
||||
assert.IsType(t, FieldMetaMap{}, IssueType{}.Fields)
|
||||
}
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
)
|
||||
|
||||
@@ -12,7 +10,7 @@ func (j *Jira) GetProjectComponents(project string) (*jiradata.Components, error
|
||||
}
|
||||
|
||||
func GetProjectComponents(ua HttpClient, endpoint string, project string) (*jiradata.Components, error) {
|
||||
uri := fmt.Sprintf("%s/rest/api/2/project/%s/components", endpoint, project)
|
||||
uri := URLJoin(endpoint, "rest/api/2/project", project, "components")
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -83,7 +83,7 @@ func Search(ua HttpClient, endpoint string, sp SearchProvider) (*jiradata.Search
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/api/2/search", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/api/2/search")
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+4
-5
@@ -3,7 +3,6 @@ package jira
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/Netflix-Skunkworks/go-jira.v1/jiradata"
|
||||
)
|
||||
@@ -17,7 +16,7 @@ type AuthOptions struct {
|
||||
Password string
|
||||
}
|
||||
|
||||
func (a *AuthOptions) AuthParams() *jiradata.AuthParams {
|
||||
func (a *AuthOptions) ProvideAuthParams() *jiradata.AuthParams {
|
||||
return &jiradata.AuthParams{
|
||||
Username: a.Username,
|
||||
Password: a.Password,
|
||||
@@ -35,7 +34,7 @@ func NewSession(ua HttpClient, endpoint string, ap AuthProvider) (*jiradata.Auth
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s/rest/auth/1/session", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/auth/1/session")
|
||||
resp, err := ua.Post(uri, "application/json", bytes.NewBuffer(encoded))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -55,7 +54,7 @@ func (j *Jira) GetSession() (*jiradata.CurrentUser, error) {
|
||||
}
|
||||
|
||||
func GetSession(ua HttpClient, endpoint string) (*jiradata.CurrentUser, error) {
|
||||
uri := fmt.Sprintf("%s/rest/auth/1/session", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/auth/1/session")
|
||||
resp, err := ua.GetJSON(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -75,7 +74,7 @@ func (j *Jira) DeleteSession() error {
|
||||
}
|
||||
|
||||
func DeleteSession(ua HttpClient, endpoint string) error {
|
||||
uri := fmt.Sprintf("%s/rest/auth/1/session", endpoint)
|
||||
uri := URLJoin(endpoint, "rest/auth/1/session")
|
||||
resp, err := ua.Delete(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,6 +3,7 @@ config:
|
||||
password-source: pass
|
||||
endpoint: https://go-jira.atlassian.net
|
||||
user: gojira
|
||||
login: gojira@corybennett.org
|
||||
|
||||
project: BASIC
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+1
-1
@@ -245,7 +245,7 @@ EOF
|
||||
# reset login for mothra for voting
|
||||
###############################################################################
|
||||
|
||||
jira="$jira --user mothra"
|
||||
jira="$jira --user mothra --login mothra@corybennett.org"
|
||||
|
||||
RUNS $jira logout
|
||||
RUNS $jira login
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ EOF
|
||||
# reset login for mothra for voting
|
||||
###############################################################################
|
||||
|
||||
jira="$jira --user mothra"
|
||||
jira="$jira --user mothra --login mothra@corybennett.org"
|
||||
|
||||
RUNS $jira logout
|
||||
echo "mothra123" | RUNS $jira login
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ EOF
|
||||
# reset login for mothra for voting
|
||||
###############################################################################
|
||||
|
||||
jira="$jira --user mothra"
|
||||
jira="$jira --user mothra --login mothra@corybennett.org"
|
||||
|
||||
RUNS $jira logout
|
||||
echo "mothra123" | RUNS $jira login
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ EOF
|
||||
# reset login for mothra for voting
|
||||
###############################################################################
|
||||
|
||||
jira="$jira --user mothra"
|
||||
jira="$jira --user mothra --login mothra@corybennett.org"
|
||||
|
||||
RUNS $jira logout
|
||||
echo "mothra123" | RUNS $jira login
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ EOF
|
||||
# reset login for mothra for voting
|
||||
###############################################################################
|
||||
|
||||
jira="$jira --user mothra"
|
||||
jira="$jira --user mothra --login mothra@corybennett.org"
|
||||
|
||||
RUNS $jira logout
|
||||
echo "mothra123" | RUNS $jira login
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@ EOF
|
||||
# reset login for mothra for voting
|
||||
###############################################################################
|
||||
|
||||
jira="$jira --user mothra"
|
||||
jira="$jira --user mothra --login mothra@corybennett.org"
|
||||
|
||||
RUNS $jira logout
|
||||
echo "mothra123" | RUNS $jira login
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
|
||||
<properties>
|
||||
<jira.version>7.2.0</jira.version>
|
||||
<amps.version>6.2.6</amps.version>
|
||||
<amps.version>6.3.15</amps.version>
|
||||
<plugin.testrunner.version>1.2.3</plugin.testrunner.version>
|
||||
<atlassian.spring.scanner.version>1.2.13</atlassian.spring.scanner.version>
|
||||
<!-- This key is used to keep the consistency between the key in atlassian-plugin.xml and the key to generate bundle. -->
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
func readJSON(input io.Reader, data interface{}) error {
|
||||
@@ -21,3 +23,13 @@ func readJSON(input io.Reader, data interface{}) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func URLJoin(endpoint string, paths ...string) string {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Unable to parse endpoint: %s", endpoint))
|
||||
}
|
||||
paths = append([]string{u.Path}, paths...)
|
||||
u.Path = path.Join(paths...)
|
||||
return u.String()
|
||||
}
|
||||
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
# Go's `text/template` package with newline elision
|
||||
|
||||
This is a fork of Go 1.4's [text/template](http://golang.org/pkg/text/template/) package with one addition: a backslash immediately after a closing delimiter will delete all subsequent newlines until a non-newline.
|
||||
|
||||
eg.
|
||||
|
||||
```
|
||||
{{if true}}\
|
||||
hello
|
||||
{{end}}\
|
||||
```
|
||||
|
||||
Will result in:
|
||||
|
||||
```
|
||||
hello\n
|
||||
```
|
||||
|
||||
Rather than:
|
||||
|
||||
```
|
||||
\n
|
||||
hello\n
|
||||
\n
|
||||
```
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
)
|
||||
|
||||
func ExampleTemplate() {
|
||||
// Define a template.
|
||||
const letter = `
|
||||
Dear {{.Name}},
|
||||
{{if .Attended}}
|
||||
It was a pleasure to see you at the wedding.{{else}}
|
||||
It is a shame you couldn't make it to the wedding.{{end}}
|
||||
{{with .Gift}}Thank you for the lovely {{.}}.
|
||||
{{end}}
|
||||
Best wishes,
|
||||
Josie
|
||||
`
|
||||
|
||||
// Prepare some data to insert into the template.
|
||||
type Recipient struct {
|
||||
Name, Gift string
|
||||
Attended bool
|
||||
}
|
||||
var recipients = []Recipient{
|
||||
{"Aunt Mildred", "bone china tea set", true},
|
||||
{"Uncle John", "moleskin pants", false},
|
||||
{"Cousin Rodney", "", false},
|
||||
}
|
||||
|
||||
// Create a new template and parse the letter into it.
|
||||
t := template.Must(template.New("letter").Parse(letter))
|
||||
|
||||
// Execute the template for each recipient.
|
||||
for _, r := range recipients {
|
||||
err := t.Execute(os.Stdout, r)
|
||||
if err != nil {
|
||||
log.Println("executing template:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Dear Aunt Mildred,
|
||||
//
|
||||
// It was a pleasure to see you at the wedding.
|
||||
// Thank you for the lovely bone china tea set.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
//
|
||||
// Dear Uncle John,
|
||||
//
|
||||
// It is a shame you couldn't make it to the wedding.
|
||||
// Thank you for the lovely moleskin pants.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
//
|
||||
// Dear Cousin Rodney,
|
||||
//
|
||||
// It is a shame you couldn't make it to the wedding.
|
||||
//
|
||||
// Best wishes,
|
||||
// Josie
|
||||
}
|
||||
-183
@@ -1,183 +0,0 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
)
|
||||
|
||||
// templateFile defines the contents of a template to be stored in a file, for testing.
|
||||
type templateFile struct {
|
||||
name string
|
||||
contents string
|
||||
}
|
||||
|
||||
func createTestDir(files []templateFile) string {
|
||||
dir, err := ioutil.TempDir("", "template")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
f, err := os.Create(filepath.Join(dir, file.name))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.WriteString(f, file.contents)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// Here we demonstrate loading a set of templates from a directory.
|
||||
func ExampleTemplate_glob() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T0.tmpl is a plain template file that just invokes T1.
|
||||
{"T0.tmpl", `T0 invokes T1: ({{template "T1"}})`},
|
||||
// T1.tmpl defines a template, T1 that invokes T2.
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// T0.tmpl is the first name matched, so it becomes the starting template,
|
||||
// the value returned by ParseGlob.
|
||||
tmpl := template.Must(template.ParseGlob(pattern))
|
||||
|
||||
err := tmpl.Execute(os.Stdout, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("template execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// T0 invokes T1: (T1 invokes T2: (This is T2))
|
||||
}
|
||||
|
||||
// This example demonstrates one way to share some templates
|
||||
// and use them in different contexts. In this variant we add multiple driver
|
||||
// templates by hand to an existing bundle of templates.
|
||||
func ExampleTemplate_helpers() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T1.tmpl defines a template, T1 that invokes T2.
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
// T2.tmpl defines a template T2.
|
||||
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// Load the helpers.
|
||||
templates := template.Must(template.ParseGlob(pattern))
|
||||
// Add one driver template to the bunch; we do this with an explicit template definition.
|
||||
_, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\n{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing driver1: ", err)
|
||||
}
|
||||
// Add another driver template.
|
||||
_, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\n{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing driver2: ", err)
|
||||
}
|
||||
// We load all the templates before execution. This package does not require
|
||||
// that behavior but html/template's escaping does, so it's a good habit.
|
||||
err = templates.ExecuteTemplate(os.Stdout, "driver1", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("driver1 execution: %s", err)
|
||||
}
|
||||
err = templates.ExecuteTemplate(os.Stdout, "driver2", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("driver2 execution: %s", err)
|
||||
}
|
||||
// Output:
|
||||
// Driver 1 calls T1: (T1 invokes T2: (This is T2))
|
||||
// Driver 2 calls T2: (This is T2)
|
||||
}
|
||||
|
||||
// This example demonstrates how to use one group of driver
|
||||
// templates with distinct sets of helper templates.
|
||||
func ExampleTemplate_share() {
|
||||
// Here we create a temporary directory and populate it with our sample
|
||||
// template definition files; usually the template files would already
|
||||
// exist in some location known to the program.
|
||||
dir := createTestDir([]templateFile{
|
||||
// T0.tmpl is a plain template file that just invokes T1.
|
||||
{"T0.tmpl", "T0 ({{.}} version) invokes T1: ({{template `T1`}})\n"},
|
||||
// T1.tmpl defines a template, T1 that invokes T2. Note T2 is not defined
|
||||
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
|
||||
})
|
||||
// Clean up after the test; another quirk of running as an example.
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// pattern is the glob pattern used to find all the template files.
|
||||
pattern := filepath.Join(dir, "*.tmpl")
|
||||
|
||||
// Here starts the example proper.
|
||||
// Load the drivers.
|
||||
drivers := template.Must(template.ParseGlob(pattern))
|
||||
|
||||
// We must define an implementation of the T2 template. First we clone
|
||||
// the drivers, then add a definition of T2 to the template name space.
|
||||
|
||||
// 1. Clone the helper set to create a new name space from which to run them.
|
||||
first, err := drivers.Clone()
|
||||
if err != nil {
|
||||
log.Fatal("cloning helpers: ", err)
|
||||
}
|
||||
// 2. Define T2, version A, and parse it.
|
||||
_, err = first.Parse("{{define `T2`}}T2, version A{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing T2: ", err)
|
||||
}
|
||||
|
||||
// Now repeat the whole thing, using a different version of T2.
|
||||
// 1. Clone the drivers.
|
||||
second, err := drivers.Clone()
|
||||
if err != nil {
|
||||
log.Fatal("cloning drivers: ", err)
|
||||
}
|
||||
// 2. Define T2, version B, and parse it.
|
||||
_, err = second.Parse("{{define `T2`}}T2, version B{{end}}")
|
||||
if err != nil {
|
||||
log.Fatal("parsing T2: ", err)
|
||||
}
|
||||
|
||||
// Execute the templates in the reverse order to verify the
|
||||
// first is unaffected by the second.
|
||||
err = second.ExecuteTemplate(os.Stdout, "T0.tmpl", "second")
|
||||
if err != nil {
|
||||
log.Fatalf("second execution: %s", err)
|
||||
}
|
||||
err = first.ExecuteTemplate(os.Stdout, "T0.tmpl", "first")
|
||||
if err != nil {
|
||||
log.Fatalf("first: execution: %s", err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// T0 (second version) invokes T1: (T1 invokes T2: (T2, version B))
|
||||
// T0 (first version) invokes T1: (T1 invokes T2: (T2, version A))
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
)
|
||||
|
||||
// This example demonstrates a custom function to process template text.
|
||||
// It installs the strings.Title function and uses it to
|
||||
// Make Title Text Look Good In Our Template's Output.
|
||||
func ExampleTemplate_func() {
|
||||
// First we create a FuncMap with which to register the function.
|
||||
funcMap := template.FuncMap{
|
||||
// The name "title" is what the function will be called in the template text.
|
||||
"title": strings.Title,
|
||||
}
|
||||
|
||||
// A simple template definition to test our function.
|
||||
// We print the input text several ways:
|
||||
// - the original
|
||||
// - title-cased
|
||||
// - title-cased and then printed with %q
|
||||
// - printed with %q and then title-cased.
|
||||
const templateText = `
|
||||
Input: {{printf "%q" .}}
|
||||
Output 0: {{title .}}
|
||||
Output 1: {{title . | printf "%q"}}
|
||||
Output 2: {{printf "%q" . | title}}
|
||||
`
|
||||
|
||||
// Create a template, add the function map, and parse the text.
|
||||
tmpl, err := template.New("titleTest").Funcs(funcMap).Parse(templateText)
|
||||
if err != nil {
|
||||
log.Fatalf("parsing: %s", err)
|
||||
}
|
||||
|
||||
// Run the template to verify the output.
|
||||
err = tmpl.Execute(os.Stdout, "the go programming language")
|
||||
if err != nil {
|
||||
log.Fatalf("execution: %s", err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Input: "the go programming language"
|
||||
// Output 0: The Go Programming Language
|
||||
// Output 1: "The Go Programming Language"
|
||||
// Output 2: "The Go Programming Language"
|
||||
}
|
||||
-1044
File diff suppressed because it is too large
Load Diff
-293
@@ -1,293 +0,0 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package template
|
||||
|
||||
// Tests for mulitple-template parsing and execution.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/template/parse"
|
||||
)
|
||||
|
||||
const (
|
||||
noError = true
|
||||
hasError = false
|
||||
)
|
||||
|
||||
type multiParseTest struct {
|
||||
name string
|
||||
input string
|
||||
ok bool
|
||||
names []string
|
||||
results []string
|
||||
}
|
||||
|
||||
var multiParseTests = []multiParseTest{
|
||||
{"empty", "", noError,
|
||||
nil,
|
||||
nil},
|
||||
{"one", `{{define "foo"}} FOO {{end}}`, noError,
|
||||
[]string{"foo"},
|
||||
[]string{" FOO "}},
|
||||
{"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
|
||||
[]string{"foo", "bar"},
|
||||
[]string{" FOO ", " BAR "}},
|
||||
// errors
|
||||
{"missing end", `{{define "foo"}} FOO `, hasError,
|
||||
nil,
|
||||
nil},
|
||||
{"malformed name", `{{define "foo}} FOO `, hasError,
|
||||
nil,
|
||||
nil},
|
||||
}
|
||||
|
||||
func TestMultiParse(t *testing.T) {
|
||||
for _, test := range multiParseTests {
|
||||
template, err := New("root").Parse(test.input)
|
||||
switch {
|
||||
case err == nil && !test.ok:
|
||||
t.Errorf("%q: expected error; got none", test.name)
|
||||
continue
|
||||
case err != nil && test.ok:
|
||||
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
case err != nil && !test.ok:
|
||||
// expected error, got one
|
||||
if *debug {
|
||||
fmt.Printf("%s: %s\n\t%s\n", test.name, test.input, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if template == nil {
|
||||
continue
|
||||
}
|
||||
if len(template.tmpl) != len(test.names)+1 { // +1 for root
|
||||
t.Errorf("%s: wrong number of templates; wanted %d got %d", test.name, len(test.names), len(template.tmpl))
|
||||
continue
|
||||
}
|
||||
for i, name := range test.names {
|
||||
tmpl, ok := template.tmpl[name]
|
||||
if !ok {
|
||||
t.Errorf("%s: can't find template %q", test.name, name)
|
||||
continue
|
||||
}
|
||||
result := tmpl.Root.String()
|
||||
if result != test.results[i] {
|
||||
t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.results[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var multiExecTests = []execTest{
|
||||
{"empty", "", "", nil, true},
|
||||
{"text", "some text", "some text", nil, true},
|
||||
{"invoke x", `{{template "x" .SI}}`, "TEXT", tVal, true},
|
||||
{"invoke x no args", `{{template "x"}}`, "TEXT", tVal, true},
|
||||
{"invoke dot int", `{{template "dot" .I}}`, "17", tVal, true},
|
||||
{"invoke dot []int", `{{template "dot" .SI}}`, "[3 4 5]", tVal, true},
|
||||
{"invoke dotV", `{{template "dotV" .U}}`, "v", tVal, true},
|
||||
{"invoke nested int", `{{template "nested" .I}}`, "17", tVal, true},
|
||||
{"variable declared by template", `{{template "nested" $x:=.SI}},{{index $x 1}}`, "[3 4 5],4", tVal, true},
|
||||
|
||||
// User-defined function: test argument evaluator.
|
||||
{"testFunc literal", `{{oneArg "joe"}}`, "oneArg=joe", tVal, true},
|
||||
{"testFunc .", `{{oneArg .}}`, "oneArg=joe", "joe", true},
|
||||
}
|
||||
|
||||
// These strings are also in testdata/*.
|
||||
const multiText1 = `
|
||||
{{define "x"}}TEXT{{end}}
|
||||
{{define "dotV"}}{{.V}}{{end}}
|
||||
`
|
||||
|
||||
const multiText2 = `
|
||||
{{define "dot"}}{{.}}{{end}}
|
||||
{{define "nested"}}{{template "dot" .}}{{end}}
|
||||
`
|
||||
|
||||
func TestMultiExecute(t *testing.T) {
|
||||
// Declare a couple of templates first.
|
||||
template, err := New("root").Parse(multiText1)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error for 1: %s", err)
|
||||
}
|
||||
_, err = template.Parse(multiText2)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error for 2: %s", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseFiles(t *testing.T) {
|
||||
_, err := ParseFiles("DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
template := New("root")
|
||||
_, err = template.ParseFiles("testdata/file1.tmpl", "testdata/file2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseGlob(t *testing.T) {
|
||||
_, err := ParseGlob("DOES NOT EXIST")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent file; got none")
|
||||
}
|
||||
_, err = New("error").ParseGlob("[x")
|
||||
if err == nil {
|
||||
t.Error("expected error for bad pattern; got none")
|
||||
}
|
||||
template := New("root")
|
||||
_, err = template.ParseGlob("testdata/file*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(multiExecTests, template, t)
|
||||
}
|
||||
|
||||
// In these tests, actual content (not just template definitions) comes from the parsed files.
|
||||
|
||||
var templateFileExecTests = []execTest{
|
||||
{"test", `{{template "tmpl1.tmpl"}}{{template "tmpl2.tmpl"}}`, "template1\n\ny\ntemplate2\n\nx\n", 0, true},
|
||||
}
|
||||
|
||||
func TestParseFilesWithData(t *testing.T) {
|
||||
template, err := New("root").ParseFiles("testdata/tmpl1.tmpl", "testdata/tmpl2.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
func TestParseGlobWithData(t *testing.T) {
|
||||
template, err := New("root").ParseGlob("testdata/tmpl*.tmpl")
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing files: %v", err)
|
||||
}
|
||||
testExecute(templateFileExecTests, template, t)
|
||||
}
|
||||
|
||||
const (
|
||||
cloneText1 = `{{define "a"}}{{template "b"}}{{template "c"}}{{end}}`
|
||||
cloneText2 = `{{define "b"}}b{{end}}`
|
||||
cloneText3 = `{{define "c"}}root{{end}}`
|
||||
cloneText4 = `{{define "c"}}clone{{end}}`
|
||||
)
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
// Create some templates and clone the root.
|
||||
root, err := New("root").Parse(cloneText1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = root.Parse(cloneText2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clone := Must(root.Clone())
|
||||
// Add variants to both.
|
||||
_, err = root.Parse(cloneText3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = clone.Parse(cloneText4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Verify that the clone is self-consistent.
|
||||
for k, v := range clone.tmpl {
|
||||
if k == clone.name && v.tmpl[k] != clone {
|
||||
t.Error("clone does not contain root")
|
||||
}
|
||||
if v != v.tmpl[v.name] {
|
||||
t.Errorf("clone does not contain self for %q", k)
|
||||
}
|
||||
}
|
||||
// Execute root.
|
||||
var b bytes.Buffer
|
||||
err = root.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "broot" {
|
||||
t.Errorf("expected %q got %q", "broot", b.String())
|
||||
}
|
||||
// Execute copy.
|
||||
b.Reset()
|
||||
err = clone.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "bclone" {
|
||||
t.Errorf("expected %q got %q", "bclone", b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddParseTree(t *testing.T) {
|
||||
// Create some templates.
|
||||
root, err := New("root").Parse(cloneText1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = root.Parse(cloneText2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Add a new parse tree.
|
||||
tree, err := parse.Parse("cloneText3", cloneText3, "", "", nil, builtins)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
added, err := root.AddParseTree("c", tree["c"])
|
||||
// Execute.
|
||||
var b bytes.Buffer
|
||||
err = added.ExecuteTemplate(&b, "a", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.String() != "broot" {
|
||||
t.Errorf("expected %q got %q", "broot", b.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 7032
|
||||
func TestAddParseTreeToUnparsedTemplate(t *testing.T) {
|
||||
master := "{{define \"master\"}}{{end}}"
|
||||
tmpl := New("master")
|
||||
tree, err := parse.Parse("master", master, "", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected parse err: %v", err)
|
||||
}
|
||||
masterTree := tree["master"]
|
||||
tmpl.AddParseTree("master", masterTree) // used to panic
|
||||
}
|
||||
|
||||
func TestRedefinition(t *testing.T) {
|
||||
var tmpl *Template
|
||||
var err error
|
||||
if tmpl, err = New("tmpl1").Parse(`{{define "test"}}foo{{end}}`); err != nil {
|
||||
t.Fatalf("parse 1: %v", err)
|
||||
}
|
||||
if _, err = tmpl.Parse(`{{define "test"}}bar{{end}}`); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "redefinition") {
|
||||
t.Fatalf("expected redefinition error; got %v", err)
|
||||
}
|
||||
if _, err = tmpl.New("tmpl2").Parse(`{{define "test"}}bar{{end}}`); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "redefinition") {
|
||||
t.Fatalf("expected redefinition error; got %v", err)
|
||||
}
|
||||
}
|
||||
-468
@@ -1,468 +0,0 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Make the types prettyprint.
|
||||
var itemName = map[itemType]string{
|
||||
itemError: "error",
|
||||
itemBool: "bool",
|
||||
itemChar: "char",
|
||||
itemCharConstant: "charconst",
|
||||
itemComplex: "complex",
|
||||
itemColonEquals: ":=",
|
||||
itemEOF: "EOF",
|
||||
itemField: "field",
|
||||
itemIdentifier: "identifier",
|
||||
itemLeftDelim: "left delim",
|
||||
itemLeftParen: "(",
|
||||
itemNumber: "number",
|
||||
itemPipe: "pipe",
|
||||
itemRawString: "raw string",
|
||||
itemRightDelim: "right delim",
|
||||
itemElideNewline: "elide newline",
|
||||
itemRightParen: ")",
|
||||
itemSpace: "space",
|
||||
itemString: "string",
|
||||
itemVariable: "variable",
|
||||
|
||||
// keywords
|
||||
itemDot: ".",
|
||||
itemDefine: "define",
|
||||
itemElse: "else",
|
||||
itemIf: "if",
|
||||
itemEnd: "end",
|
||||
itemNil: "nil",
|
||||
itemRange: "range",
|
||||
itemTemplate: "template",
|
||||
itemWith: "with",
|
||||
}
|
||||
|
||||
func (i itemType) String() string {
|
||||
s := itemName[i]
|
||||
if s == "" {
|
||||
return fmt.Sprintf("item%d", int(i))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type lexTest struct {
|
||||
name string
|
||||
input string
|
||||
items []item
|
||||
}
|
||||
|
||||
var (
|
||||
tEOF = item{itemEOF, 0, ""}
|
||||
tFor = item{itemIdentifier, 0, "for"}
|
||||
tLeft = item{itemLeftDelim, 0, "{{"}
|
||||
tLpar = item{itemLeftParen, 0, "("}
|
||||
tPipe = item{itemPipe, 0, "|"}
|
||||
tQuote = item{itemString, 0, `"abc \n\t\" "`}
|
||||
tRange = item{itemRange, 0, "range"}
|
||||
tRight = item{itemRightDelim, 0, "}}"}
|
||||
tElideNewline = item{itemElideNewline, 0, "\\"}
|
||||
tRpar = item{itemRightParen, 0, ")"}
|
||||
tSpace = item{itemSpace, 0, " "}
|
||||
raw = "`" + `abc\n\t\" ` + "`"
|
||||
tRawQuote = item{itemRawString, 0, raw}
|
||||
)
|
||||
|
||||
var lexTests = []lexTest{
|
||||
{"empty", "", []item{tEOF}},
|
||||
{"spaces", " \t\n", []item{{itemText, 0, " \t\n"}, tEOF}},
|
||||
{"text", `now is the time`, []item{{itemText, 0, "now is the time"}, tEOF}},
|
||||
{"elide newline", "{{}}\\", []item{tLeft, tRight, tElideNewline, tEOF}},
|
||||
{"text with comment", "hello-{{/* this is a comment */}}-world", []item{
|
||||
{itemText, 0, "hello-"},
|
||||
{itemText, 0, "-world"},
|
||||
tEOF,
|
||||
}},
|
||||
{"punctuation", "{{,@% }}", []item{
|
||||
tLeft,
|
||||
{itemChar, 0, ","},
|
||||
{itemChar, 0, "@"},
|
||||
{itemChar, 0, "%"},
|
||||
tSpace,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"parens", "{{((3))}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
tLpar,
|
||||
{itemNumber, 0, "3"},
|
||||
tRpar,
|
||||
tRpar,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
|
||||
{"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
|
||||
{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
|
||||
{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
|
||||
{"numbers", "{{1 02 0x14 -7.2i 1e3 +1.2e-4 4.2i 1+2i}}", []item{
|
||||
tLeft,
|
||||
{itemNumber, 0, "1"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "02"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "0x14"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "-7.2i"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "1e3"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "+1.2e-4"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "4.2i"},
|
||||
tSpace,
|
||||
{itemComplex, 0, "1+2i"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"characters", `{{'a' '\n' '\'' '\\' '\u00FF' '\xFF' '本'}}`, []item{
|
||||
tLeft,
|
||||
{itemCharConstant, 0, `'a'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\n'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\''`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\\'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\u00FF'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'\xFF'`},
|
||||
tSpace,
|
||||
{itemCharConstant, 0, `'本'`},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"bools", "{{true false}}", []item{
|
||||
tLeft,
|
||||
{itemBool, 0, "true"},
|
||||
tSpace,
|
||||
{itemBool, 0, "false"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"dot", "{{.}}", []item{
|
||||
tLeft,
|
||||
{itemDot, 0, "."},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"nil", "{{nil}}", []item{
|
||||
tLeft,
|
||||
{itemNil, 0, "nil"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"dots", "{{.x . .2 .x.y.z}}", []item{
|
||||
tLeft,
|
||||
{itemField, 0, ".x"},
|
||||
tSpace,
|
||||
{itemDot, 0, "."},
|
||||
tSpace,
|
||||
{itemNumber, 0, ".2"},
|
||||
tSpace,
|
||||
{itemField, 0, ".x"},
|
||||
{itemField, 0, ".y"},
|
||||
{itemField, 0, ".z"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"keywords", "{{range if else end with}}", []item{
|
||||
tLeft,
|
||||
{itemRange, 0, "range"},
|
||||
tSpace,
|
||||
{itemIf, 0, "if"},
|
||||
tSpace,
|
||||
{itemElse, 0, "else"},
|
||||
tSpace,
|
||||
{itemEnd, 0, "end"},
|
||||
tSpace,
|
||||
{itemWith, 0, "with"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$c"},
|
||||
tSpace,
|
||||
{itemColonEquals, 0, ":="},
|
||||
tSpace,
|
||||
{itemIdentifier, 0, "printf"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$hello"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$23"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$"},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$var"},
|
||||
{itemField, 0, ".Field"},
|
||||
tSpace,
|
||||
{itemField, 0, ".Method"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"variable invocation", "{{$x 23}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$x"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "23"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{
|
||||
{itemText, 0, "intro "},
|
||||
tLeft,
|
||||
{itemIdentifier, 0, "echo"},
|
||||
tSpace,
|
||||
{itemIdentifier, 0, "hi"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "1.2"},
|
||||
tSpace,
|
||||
tPipe,
|
||||
{itemIdentifier, 0, "noargs"},
|
||||
tPipe,
|
||||
{itemIdentifier, 0, "args"},
|
||||
tSpace,
|
||||
{itemNumber, 0, "1"},
|
||||
tSpace,
|
||||
{itemString, 0, `"hi"`},
|
||||
tRight,
|
||||
{itemText, 0, " outro"},
|
||||
tEOF,
|
||||
}},
|
||||
{"declaration", "{{$v := 3}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$v"},
|
||||
tSpace,
|
||||
{itemColonEquals, 0, ":="},
|
||||
tSpace,
|
||||
{itemNumber, 0, "3"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"2 declarations", "{{$v , $w := 3}}", []item{
|
||||
tLeft,
|
||||
{itemVariable, 0, "$v"},
|
||||
tSpace,
|
||||
{itemChar, 0, ","},
|
||||
tSpace,
|
||||
{itemVariable, 0, "$w"},
|
||||
tSpace,
|
||||
{itemColonEquals, 0, ":="},
|
||||
tSpace,
|
||||
{itemNumber, 0, "3"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"field of parenthesized expression", "{{(.X).Y}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
{itemField, 0, ".X"},
|
||||
tRpar,
|
||||
{itemField, 0, ".Y"},
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
// errors
|
||||
{"badchar", "#{{\x01}}", []item{
|
||||
{itemText, 0, "#"},
|
||||
tLeft,
|
||||
{itemError, 0, "unrecognized character in action: U+0001"},
|
||||
}},
|
||||
{"unclosed action", "{{\n}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unclosed action"},
|
||||
}},
|
||||
{"EOF in action", "{{range", []item{
|
||||
tLeft,
|
||||
tRange,
|
||||
{itemError, 0, "unclosed action"},
|
||||
}},
|
||||
{"unclosed quote", "{{\"\n\"}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unterminated quoted string"},
|
||||
}},
|
||||
{"unclosed raw quote", "{{`xx\n`}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unterminated raw quoted string"},
|
||||
}},
|
||||
{"unclosed char constant", "{{'\n}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, "unterminated character constant"},
|
||||
}},
|
||||
{"bad number", "{{3k}}", []item{
|
||||
tLeft,
|
||||
{itemError, 0, `bad number syntax: "3k"`},
|
||||
}},
|
||||
{"unclosed paren", "{{(3}}", []item{
|
||||
tLeft,
|
||||
tLpar,
|
||||
{itemNumber, 0, "3"},
|
||||
{itemError, 0, `unclosed left paren`},
|
||||
}},
|
||||
{"extra right paren", "{{3)}}", []item{
|
||||
tLeft,
|
||||
{itemNumber, 0, "3"},
|
||||
tRpar,
|
||||
{itemError, 0, `unexpected right paren U+0029 ')'`},
|
||||
}},
|
||||
|
||||
// Fixed bugs
|
||||
// Many elements in an action blew the lookahead until
|
||||
// we made lexInsideAction not loop.
|
||||
{"long pipeline deadlock", "{{|||||}}", []item{
|
||||
tLeft,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tPipe,
|
||||
tRight,
|
||||
tEOF,
|
||||
}},
|
||||
{"text with bad comment", "hello-{{/*/}}-world", []item{
|
||||
{itemText, 0, "hello-"},
|
||||
{itemError, 0, `unclosed comment`},
|
||||
}},
|
||||
{"text with comment close separted from delim", "hello-{{/* */ }}-world", []item{
|
||||
{itemText, 0, "hello-"},
|
||||
{itemError, 0, `comment ends before closing delimiter`},
|
||||
}},
|
||||
// This one is an error that we can't catch because it breaks templates with
|
||||
// minimized JavaScript. Should have fixed it before Go 1.1.
|
||||
{"unmatched right delimiter", "hello-{.}}-world", []item{
|
||||
{itemText, 0, "hello-{.}}-world"},
|
||||
tEOF,
|
||||
}},
|
||||
}
|
||||
|
||||
// collect gathers the emitted items into a slice.
|
||||
func collect(t *lexTest, left, right string) (items []item) {
|
||||
l := lex(t.name, t.input, left, right)
|
||||
for {
|
||||
item := l.nextItem()
|
||||
items = append(items, item)
|
||||
if item.typ == itemEOF || item.typ == itemError {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func equal(i1, i2 []item, checkPos bool) bool {
|
||||
if len(i1) != len(i2) {
|
||||
return false
|
||||
}
|
||||
for k := range i1 {
|
||||
if i1[k].typ != i2[k].typ {
|
||||
return false
|
||||
}
|
||||
if i1[k].val != i2[k].val {
|
||||
return false
|
||||
}
|
||||
if checkPos && i1[k].pos != i2[k].pos {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestLex(t *testing.T) {
|
||||
for _, test := range lexTests {
|
||||
items := collect(&test, "", "")
|
||||
if !equal(items, test.items, false) {
|
||||
t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, items, test.items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some easy cases from above, but with delimiters $$ and @@
|
||||
var lexDelimTests = []lexTest{
|
||||
{"punctuation", "$$,@%{{}}@@", []item{
|
||||
tLeftDelim,
|
||||
{itemChar, 0, ","},
|
||||
{itemChar, 0, "@"},
|
||||
{itemChar, 0, "%"},
|
||||
{itemChar, 0, "{"},
|
||||
{itemChar, 0, "{"},
|
||||
{itemChar, 0, "}"},
|
||||
{itemChar, 0, "}"},
|
||||
tRightDelim,
|
||||
tEOF,
|
||||
}},
|
||||
{"empty action", `$$@@`, []item{tLeftDelim, tRightDelim, tEOF}},
|
||||
{"for", `$$for@@`, []item{tLeftDelim, tFor, tRightDelim, tEOF}},
|
||||
{"quote", `$$"abc \n\t\" "@@`, []item{tLeftDelim, tQuote, tRightDelim, tEOF}},
|
||||
{"raw quote", "$$" + raw + "@@", []item{tLeftDelim, tRawQuote, tRightDelim, tEOF}},
|
||||
}
|
||||
|
||||
var (
|
||||
tLeftDelim = item{itemLeftDelim, 0, "$$"}
|
||||
tRightDelim = item{itemRightDelim, 0, "@@"}
|
||||
)
|
||||
|
||||
func TestDelims(t *testing.T) {
|
||||
for _, test := range lexDelimTests {
|
||||
items := collect(&test, "$$", "@@")
|
||||
if !equal(items, test.items, false) {
|
||||
t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lexPosTests = []lexTest{
|
||||
{"empty", "", []item{tEOF}},
|
||||
{"punctuation", "{{,@%#}}", []item{
|
||||
{itemLeftDelim, 0, "{{"},
|
||||
{itemChar, 2, ","},
|
||||
{itemChar, 3, "@"},
|
||||
{itemChar, 4, "%"},
|
||||
{itemChar, 5, "#"},
|
||||
{itemRightDelim, 6, "}}"},
|
||||
{itemEOF, 8, ""},
|
||||
}},
|
||||
{"sample", "0123{{hello}}xyz", []item{
|
||||
{itemText, 0, "0123"},
|
||||
{itemLeftDelim, 4, "{{"},
|
||||
{itemIdentifier, 6, "hello"},
|
||||
{itemRightDelim, 11, "}}"},
|
||||
{itemText, 13, "xyz"},
|
||||
{itemEOF, 16, ""},
|
||||
}},
|
||||
}
|
||||
|
||||
// The other tests don't check position, to make the test cases easier to construct.
|
||||
// This one does.
|
||||
func TestPos(t *testing.T) {
|
||||
for _, test := range lexPosTests {
|
||||
items := collect(&test, "", "")
|
||||
if !equal(items, test.items, true) {
|
||||
t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
|
||||
if len(items) == len(test.items) {
|
||||
// Detailed print; avoid item.String() to expose the position value.
|
||||
for i := range items {
|
||||
if !equal(items[i:i+1], test.items[i:i+1], true) {
|
||||
i1 := items[i]
|
||||
i2 := test.items[i]
|
||||
t.Errorf("\t#%d: got {%v %d %q} expected {%v %d %q}", i, i1.typ, i1.pos, i1.val, i2.typ, i2.pos, i2.val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-426
@@ -1,426 +0,0 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var debug = flag.Bool("debug", false, "show the errors produced by the main tests")
|
||||
|
||||
type numberTest struct {
|
||||
text string
|
||||
isInt bool
|
||||
isUint bool
|
||||
isFloat bool
|
||||
isComplex bool
|
||||
int64
|
||||
uint64
|
||||
float64
|
||||
complex128
|
||||
}
|
||||
|
||||
var numberTests = []numberTest{
|
||||
// basics
|
||||
{"0", true, true, true, false, 0, 0, 0, 0},
|
||||
{"-0", true, true, true, false, 0, 0, 0, 0}, // check that -0 is a uint.
|
||||
{"73", true, true, true, false, 73, 73, 73, 0},
|
||||
{"073", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"-73", true, false, true, false, -73, 0, -73, 0},
|
||||
{"+73", true, false, true, false, 73, 0, 73, 0},
|
||||
{"100", true, true, true, false, 100, 100, 100, 0},
|
||||
{"1e9", true, true, true, false, 1e9, 1e9, 1e9, 0},
|
||||
{"-1e9", true, false, true, false, -1e9, 0, -1e9, 0},
|
||||
{"-1.2", false, false, true, false, 0, 0, -1.2, 0},
|
||||
{"1e19", false, true, true, false, 0, 1e19, 1e19, 0},
|
||||
{"-1e19", false, false, true, false, 0, 0, -1e19, 0},
|
||||
{"4i", false, false, false, true, 0, 0, 0, 4i},
|
||||
{"-1.2+4.2i", false, false, false, true, 0, 0, 0, -1.2 + 4.2i},
|
||||
{"073i", false, false, false, true, 0, 0, 0, 73i}, // not octal!
|
||||
// complex with 0 imaginary are float (and maybe integer)
|
||||
{"0i", true, true, true, true, 0, 0, 0, 0},
|
||||
{"-1.2+0i", false, false, true, true, 0, 0, -1.2, -1.2},
|
||||
{"-12+0i", true, false, true, true, -12, 0, -12, -12},
|
||||
{"13+0i", true, true, true, true, 13, 13, 13, 13},
|
||||
// funny bases
|
||||
{"0123", true, true, true, false, 0123, 0123, 0123, 0},
|
||||
{"-0x0", true, true, true, false, 0, 0, 0, 0},
|
||||
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
|
||||
// character constants
|
||||
{`'a'`, true, true, true, false, 'a', 'a', 'a', 0},
|
||||
{`'\n'`, true, true, true, false, '\n', '\n', '\n', 0},
|
||||
{`'\\'`, true, true, true, false, '\\', '\\', '\\', 0},
|
||||
{`'\''`, true, true, true, false, '\'', '\'', '\'', 0},
|
||||
{`'\xFF'`, true, true, true, false, 0xFF, 0xFF, 0xFF, 0},
|
||||
{`'パ'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||
{`'\u30d1'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||
{`'\U000030d1'`, true, true, true, false, 0x30d1, 0x30d1, 0x30d1, 0},
|
||||
// some broken syntax
|
||||
{text: "+-2"},
|
||||
{text: "0x123."},
|
||||
{text: "1e."},
|
||||
{text: "0xi."},
|
||||
{text: "1+2."},
|
||||
{text: "'x"},
|
||||
{text: "'xx'"},
|
||||
// Issue 8622 - 0xe parsed as floating point. Very embarrassing.
|
||||
{"0xef", true, true, true, false, 0xef, 0xef, 0xef, 0},
|
||||
}
|
||||
|
||||
func TestNumberParse(t *testing.T) {
|
||||
for _, test := range numberTests {
|
||||
// If fmt.Sscan thinks it's complex, it's complex. We can't trust the output
|
||||
// because imaginary comes out as a number.
|
||||
var c complex128
|
||||
typ := itemNumber
|
||||
var tree *Tree
|
||||
if test.text[0] == '\'' {
|
||||
typ = itemCharConstant
|
||||
} else {
|
||||
_, err := fmt.Sscan(test.text, &c)
|
||||
if err == nil {
|
||||
typ = itemComplex
|
||||
}
|
||||
}
|
||||
n, err := tree.newNumber(0, test.text, typ)
|
||||
ok := test.isInt || test.isUint || test.isFloat || test.isComplex
|
||||
if ok && err != nil {
|
||||
t.Errorf("unexpected error for %q: %s", test.text, err)
|
||||
continue
|
||||
}
|
||||
if !ok && err == nil {
|
||||
t.Errorf("expected error for %q", test.text)
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
if *debug {
|
||||
fmt.Printf("%s\n\t%s\n", test.text, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if n.IsComplex != test.isComplex {
|
||||
t.Errorf("complex incorrect for %q; should be %t", test.text, test.isComplex)
|
||||
}
|
||||
if test.isInt {
|
||||
if !n.IsInt {
|
||||
t.Errorf("expected integer for %q", test.text)
|
||||
}
|
||||
if n.Int64 != test.int64 {
|
||||
t.Errorf("int64 for %q should be %d Is %d", test.text, test.int64, n.Int64)
|
||||
}
|
||||
} else if n.IsInt {
|
||||
t.Errorf("did not expect integer for %q", test.text)
|
||||
}
|
||||
if test.isUint {
|
||||
if !n.IsUint {
|
||||
t.Errorf("expected unsigned integer for %q", test.text)
|
||||
}
|
||||
if n.Uint64 != test.uint64 {
|
||||
t.Errorf("uint64 for %q should be %d Is %d", test.text, test.uint64, n.Uint64)
|
||||
}
|
||||
} else if n.IsUint {
|
||||
t.Errorf("did not expect unsigned integer for %q", test.text)
|
||||
}
|
||||
if test.isFloat {
|
||||
if !n.IsFloat {
|
||||
t.Errorf("expected float for %q", test.text)
|
||||
}
|
||||
if n.Float64 != test.float64 {
|
||||
t.Errorf("float64 for %q should be %g Is %g", test.text, test.float64, n.Float64)
|
||||
}
|
||||
} else if n.IsFloat {
|
||||
t.Errorf("did not expect float for %q", test.text)
|
||||
}
|
||||
if test.isComplex {
|
||||
if !n.IsComplex {
|
||||
t.Errorf("expected complex for %q", test.text)
|
||||
}
|
||||
if n.Complex128 != test.complex128 {
|
||||
t.Errorf("complex128 for %q should be %g Is %g", test.text, test.complex128, n.Complex128)
|
||||
}
|
||||
} else if n.IsComplex {
|
||||
t.Errorf("did not expect complex for %q", test.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type parseTest struct {
|
||||
name string
|
||||
input string
|
||||
ok bool
|
||||
result string // what the user would see in an error message.
|
||||
}
|
||||
|
||||
const (
|
||||
noError = true
|
||||
hasError = false
|
||||
)
|
||||
|
||||
var parseTests = []parseTest{
|
||||
{"empty", "", noError,
|
||||
``},
|
||||
{"comment", "{{/*\n\n\n*/}}", noError,
|
||||
``},
|
||||
{"spaces", " \t\n", noError,
|
||||
`" \t\n"`},
|
||||
{"text", "some text", noError,
|
||||
`"some text"`},
|
||||
{"emptyAction", "{{}}", hasError,
|
||||
`{{}}`},
|
||||
{"field", "{{.X}}", noError,
|
||||
`{{.X}}`},
|
||||
{"simple command", "{{printf}}", noError,
|
||||
`{{printf}}`},
|
||||
{"$ invocation", "{{$}}", noError,
|
||||
"{{$}}"},
|
||||
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
|
||||
"{{with $x := 3}}{{$x 23}}{{end}}"},
|
||||
{"variable with fields", "{{$.I}}", noError,
|
||||
"{{$.I}}"},
|
||||
{"multi-word command", "{{printf `%d` 23}}", noError,
|
||||
"{{printf `%d` 23}}"},
|
||||
{"pipeline", "{{.X|.Y}}", noError,
|
||||
`{{.X | .Y}}`},
|
||||
{"pipeline with decl", "{{$x := .X|.Y}}", noError,
|
||||
`{{$x := .X | .Y}}`},
|
||||
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
|
||||
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
|
||||
{"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
|
||||
`{{(.Y .Z).Field}}`},
|
||||
{"simple if", "{{if .X}}hello{{end}}", noError,
|
||||
`{{if .X}}"hello"{{end}}`},
|
||||
{"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}"false"{{end}}`},
|
||||
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
|
||||
{"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
|
||||
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
|
||||
{"simple range", "{{range .X}}hello{{end}}", noError,
|
||||
`{{range .X}}"hello"{{end}}`},
|
||||
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
|
||||
`{{range .X.Y.Z}}"hello"{{end}}`},
|
||||
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
|
||||
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
|
||||
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X}}"true"{{else}}"false"{{end}}`},
|
||||
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X | .M}}"true"{{else}}"false"{{end}}`},
|
||||
{"range []int", "{{range .SI}}{{.}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{end}}`},
|
||||
{"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x := .SI}}{{.}}{{end}}`},
|
||||
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x, $y := .SI}}{{.}}{{end}}`},
|
||||
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
|
||||
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
|
||||
{"template", "{{template `x`}}", noError,
|
||||
`{{template "x"}}`},
|
||||
{"template with arg", "{{template `x` .Y}}", noError,
|
||||
`{{template "x" .Y}}`},
|
||||
{"with", "{{with .X}}hello{{end}}", noError,
|
||||
`{{with .X}}"hello"{{end}}`},
|
||||
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
|
||||
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
|
||||
{"elide newline", "{{true}}\\\n ", noError,
|
||||
`{{true}}" "`},
|
||||
// Errors.
|
||||
{"unclosed action", "hello{{range", hasError, ""},
|
||||
{"unmatched end", "{{end}}", hasError, ""},
|
||||
{"missing end", "hello{{range .x}}", hasError, ""},
|
||||
{"missing end after else", "hello{{range .x}}{{else}}", hasError, ""},
|
||||
{"undefined function", "hello{{undefined}}", hasError, ""},
|
||||
{"undefined variable", "{{$x}}", hasError, ""},
|
||||
{"variable undefined after end", "{{with $x := 4}}{{end}}{{$x}}", hasError, ""},
|
||||
{"variable undefined in template", "{{template $v}}", hasError, ""},
|
||||
{"declare with field", "{{with $x.Y := 4}}{{end}}", hasError, ""},
|
||||
{"template with field ref", "{{template .X}}", hasError, ""},
|
||||
{"template with var", "{{template $v}}", hasError, ""},
|
||||
{"invalid punctuation", "{{printf 3, 4}}", hasError, ""},
|
||||
{"multidecl outside range", "{{with $v, $u := 3}}{{end}}", hasError, ""},
|
||||
{"too many decls in range", "{{range $u, $v, $w := 3}}{{end}}", hasError, ""},
|
||||
{"dot applied to parentheses", "{{printf (printf .).}}", hasError, ""},
|
||||
{"adjacent args", "{{printf 3`x`}}", hasError, ""},
|
||||
{"adjacent args with .", "{{printf `x`.}}", hasError, ""},
|
||||
{"extra end after if", "{{if .X}}a{{else if .Y}}b{{end}}{{end}}", hasError, ""},
|
||||
{"invalid newline elision", "{{true}}\\{{true}}", hasError, ""},
|
||||
// Equals (and other chars) do not assignments make (yet).
|
||||
{"bug0a", "{{$x := 0}}{{$x}}", noError, "{{$x := 0}}{{$x}}"},
|
||||
{"bug0b", "{{$x = 1}}{{$x}}", hasError, ""},
|
||||
{"bug0c", "{{$x ! 2}}{{$x}}", hasError, ""},
|
||||
{"bug0d", "{{$x % 3}}{{$x}}", hasError, ""},
|
||||
// Check the parse fails for := rather than comma.
|
||||
{"bug0e", "{{range $x := $y := 3}}{{end}}", hasError, ""},
|
||||
// Another bug: variable read must ignore following punctuation.
|
||||
{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here.
|
||||
{"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""}, // $x+2 should not parse as ($x) (+2).
|
||||
{"bug1c", "{{$x:=.}}{{$x +2}}", noError, "{{$x := .}}{{$x +2}}"}, // It's OK with a space.
|
||||
}
|
||||
|
||||
var builtins = map[string]interface{}{
|
||||
"printf": fmt.Sprintf,
|
||||
}
|
||||
|
||||
func testParse(doCopy bool, t *testing.T) {
|
||||
textFormat = "%q"
|
||||
defer func() { textFormat = "%s" }()
|
||||
for _, test := range parseTests {
|
||||
tmpl, err := New(test.name).Parse(test.input, "", "", make(map[string]*Tree), builtins)
|
||||
switch {
|
||||
case err == nil && !test.ok:
|
||||
t.Errorf("%q: expected error; got none", test.name)
|
||||
continue
|
||||
case err != nil && test.ok:
|
||||
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
case err != nil && !test.ok:
|
||||
// expected error, got one
|
||||
if *debug {
|
||||
fmt.Printf("%s: %s\n\t%s\n", test.name, test.input, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
var result string
|
||||
if doCopy {
|
||||
result = tmpl.Root.Copy().String()
|
||||
} else {
|
||||
result = tmpl.Root.String()
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("%s=(%q): got\n\t%v\nexpected\n\t%v", test.name, test.input, result, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testParse(false, t)
|
||||
}
|
||||
|
||||
// Same as TestParse, but we copy the node first
|
||||
func TestParseCopy(t *testing.T) {
|
||||
testParse(true, t)
|
||||
}
|
||||
|
||||
type isEmptyTest struct {
|
||||
name string
|
||||
input string
|
||||
empty bool
|
||||
}
|
||||
|
||||
var isEmptyTests = []isEmptyTest{
|
||||
{"empty", ``, true},
|
||||
{"nonempty", `hello`, false},
|
||||
{"spaces only", " \t\n \t\n", true},
|
||||
{"definition", `{{define "x"}}something{{end}}`, true},
|
||||
{"definitions and space", "{{define `x`}}something{{end}}\n\n{{define `y`}}something{{end}}\n\n", true},
|
||||
{"definitions and text", "{{define `x`}}something{{end}}\nx\n{{define `y`}}something{{end}}\ny\n", false},
|
||||
{"definition and action", "{{define `x`}}something{{end}}{{if 3}}foo{{end}}", false},
|
||||
}
|
||||
|
||||
func TestIsEmpty(t *testing.T) {
|
||||
if !IsEmptyTree(nil) {
|
||||
t.Errorf("nil tree is not empty")
|
||||
}
|
||||
for _, test := range isEmptyTests {
|
||||
tree, err := New("root").Parse(test.input, "", "", make(map[string]*Tree), nil)
|
||||
if err != nil {
|
||||
t.Errorf("%q: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
}
|
||||
if empty := IsEmptyTree(tree.Root); empty != test.empty {
|
||||
t.Errorf("%q: expected %t got %t", test.name, test.empty, empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorContextWithTreeCopy(t *testing.T) {
|
||||
tree, err := New("root").Parse("{{if true}}{{end}}", "", "", make(map[string]*Tree), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected tree parse failure: %v", err)
|
||||
}
|
||||
treeCopy := tree.Copy()
|
||||
wantLocation, wantContext := tree.ErrorContext(tree.Root.Nodes[0])
|
||||
gotLocation, gotContext := treeCopy.ErrorContext(treeCopy.Root.Nodes[0])
|
||||
if wantLocation != gotLocation {
|
||||
t.Errorf("wrong error location want %q got %q", wantLocation, gotLocation)
|
||||
}
|
||||
if wantContext != gotContext {
|
||||
t.Errorf("wrong error location want %q got %q", wantContext, gotContext)
|
||||
}
|
||||
}
|
||||
|
||||
// All failures, and the result is a string that must appear in the error message.
|
||||
var errorTests = []parseTest{
|
||||
// Check line numbers are accurate.
|
||||
{"unclosed1",
|
||||
"line1\n{{",
|
||||
hasError, `unclosed1:2: unexpected unclosed action in command`},
|
||||
{"unclosed2",
|
||||
"line1\n{{define `x`}}line2\n{{",
|
||||
hasError, `unclosed2:3: unexpected unclosed action in command`},
|
||||
// Specific errors.
|
||||
{"function",
|
||||
"{{foo}}",
|
||||
hasError, `function "foo" not defined`},
|
||||
{"comment",
|
||||
"{{/*}}",
|
||||
hasError, `unclosed comment`},
|
||||
{"lparen",
|
||||
"{{.X (1 2 3}}",
|
||||
hasError, `unclosed left paren`},
|
||||
{"rparen",
|
||||
"{{.X 1 2 3)}}",
|
||||
hasError, `unexpected ")"`},
|
||||
{"space",
|
||||
"{{`x`3}}",
|
||||
hasError, `missing space?`},
|
||||
{"idchar",
|
||||
"{{a#}}",
|
||||
hasError, `'#'`},
|
||||
{"charconst",
|
||||
"{{'a}}",
|
||||
hasError, `unterminated character constant`},
|
||||
{"stringconst",
|
||||
`{{"a}}`,
|
||||
hasError, `unterminated quoted string`},
|
||||
{"rawstringconst",
|
||||
"{{`a}}",
|
||||
hasError, `unterminated raw quoted string`},
|
||||
{"number",
|
||||
"{{0xi}}",
|
||||
hasError, `number syntax`},
|
||||
{"multidefine",
|
||||
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
|
||||
hasError, `multiple definition of template`},
|
||||
{"eof",
|
||||
"{{range .X}}",
|
||||
hasError, `unexpected EOF`},
|
||||
{"variable",
|
||||
// Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
|
||||
"{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
|
||||
hasError, `unexpected ":="`},
|
||||
{"multidecl",
|
||||
"{{$a,$b,$c := 23}}",
|
||||
hasError, `too many declarations`},
|
||||
{"undefvar",
|
||||
"{{$a}}",
|
||||
hasError, `undefined variable`},
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
for _, test := range errorTests {
|
||||
_, err := New(test.name).Parse(test.input, "", "", make(map[string]*Tree))
|
||||
if err == nil {
|
||||
t.Errorf("%q: expected error", test.name)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(err.Error(), test.result) {
|
||||
t.Errorf("%q: error %q does not contain %q", test.name, err, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
# Units - Helpful unit multipliers and functions for Go
|
||||
|
||||
The goal of this package is to have functionality similar to the [time](http://golang.org/pkg/time/) package.
|
||||
|
||||
It allows for code like this:
|
||||
|
||||
```go
|
||||
n, err := ParseBase2Bytes("1KB")
|
||||
// n == 1024
|
||||
n = units.Mebibyte * 512
|
||||
```
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
package units
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBase2BytesString(t *testing.T) {
|
||||
assert.Equal(t, Base2Bytes(0).String(), "0B")
|
||||
assert.Equal(t, Base2Bytes(1025).String(), "1KiB1B")
|
||||
assert.Equal(t, Base2Bytes(1048577).String(), "1MiB1B")
|
||||
}
|
||||
|
||||
func TestParseBase2Bytes(t *testing.T) {
|
||||
n, err := ParseBase2Bytes("0B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, int(n))
|
||||
n, err = ParseBase2Bytes("1KB")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1024, int(n))
|
||||
n, err = ParseBase2Bytes("1MB1KB25B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1049625, int(n))
|
||||
n, err = ParseBase2Bytes("1.5MB")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1572864, int(n))
|
||||
}
|
||||
|
||||
func TestMetricBytesString(t *testing.T) {
|
||||
assert.Equal(t, MetricBytes(0).String(), "0B")
|
||||
assert.Equal(t, MetricBytes(1001).String(), "1KB1B")
|
||||
assert.Equal(t, MetricBytes(1001025).String(), "1MB1KB25B")
|
||||
}
|
||||
|
||||
func TestParseMetricBytes(t *testing.T) {
|
||||
n, err := ParseMetricBytes("0B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, int(n))
|
||||
n, err = ParseMetricBytes("1KB1B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1001, int(n))
|
||||
n, err = ParseMetricBytes("1MB1KB25B")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1001025, int(n))
|
||||
n, err = ParseMetricBytes("1.5MB")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1500000, int(n))
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
genny
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
# - 1.0
|
||||
# - 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
-245
@@ -1,245 +0,0 @@
|
||||
# genny - Generics for Go
|
||||
|
||||
[](https://travis-ci.org/cheekybits/genny) [](http://godoc.org/github.com/cheekybits/genny/parse)
|
||||
|
||||
Install:
|
||||
|
||||
```
|
||||
go get github.com/cheekybits/genny
|
||||
```
|
||||
|
||||
=====
|
||||
|
||||
(pron. Jenny) by Mat Ryer ([@matryer](https://twitter.com/matryer)) and Tyler Bunnell ([@TylerJBunnell](https://twitter.com/TylerJBunnell)).
|
||||
|
||||
Until the Go core team include support for [generics in Go](http://golang.org/doc/faq#generics), `genny` is a code-generation generics solution. It allows you write normal buildable and testable Go code which, when processed by the `genny gen` tool, will replace the generics with specific types.
|
||||
|
||||
* Generic code is valid Go code
|
||||
* Generic code compiles and can be tested
|
||||
* Use `stdin` and `stdout` or specify in and out files
|
||||
* Supports Go 1.4's [go generate](http://tip.golang.org/doc/go1.4#gogenerate)
|
||||
* Multiple specific types will generate every permutation
|
||||
* Use `BUILTINS` and `NUMBERS` wildtype to generate specific code for all built-in (and number) Go types
|
||||
* Function names and comments also get updated
|
||||
|
||||
## Library
|
||||
|
||||
We have started building a [library of common things](https://github.com/cheekybits/gennylib), and you can use `genny get` to generate the specific versions you need.
|
||||
|
||||
For example: `genny get maps/concurrentmap.go "KeyType=BUILTINS ValueType=BUILTINS"` will print out generated code for all types for a concurrent map. Any file in the library may be generated locally in this way using all the same options given to `genny gen`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
genny [{flags}] gen "{types}"
|
||||
|
||||
gen - generates type specific code from generic code.
|
||||
get <package/file> - fetch a generic template from the online library and gen it.
|
||||
|
||||
{types} - (optional) Command line flags (see below)
|
||||
{types} - (required) Specific types for each generic type in the source
|
||||
{types} format: {generic}={specific}[,another][ {generic2}={specific2}]
|
||||
|
||||
Examples:
|
||||
Generic=Specific
|
||||
Generic1=Specific1 Generic2=Specific2
|
||||
Generic1=Specific1,Specific2 Generic2=Specific3,Specific4
|
||||
|
||||
Flags:
|
||||
-in="": file to parse instead of stdin
|
||||
-out="": file to save output to instead of stdout
|
||||
-pkg="": package name for generated files
|
||||
```
|
||||
|
||||
* Comma separated type lists will generate code for each type
|
||||
|
||||
### Flags
|
||||
|
||||
* `-in` - specify the input file (rather than using stdin)
|
||||
* `-out` - specify the output file (rather than using stdout)
|
||||
|
||||
### go generate
|
||||
|
||||
To use Go 1.4's `go generate` capability, insert the following comment in your source code file:
|
||||
|
||||
```
|
||||
//go:generate genny -in=$GOFILE -out=gen-$GOFILE gen "KeyType=string,int ValueType=string,int"
|
||||
```
|
||||
|
||||
* Start the line with `//go:generate `
|
||||
* Use the `-in` and `-out` flags to specify the files to work on
|
||||
* Use the `genny` command as usual after the flags
|
||||
|
||||
Now, running `go generate` (in a shell) for the package will cause the generic versions of the files to be generated.
|
||||
|
||||
* The output file will be overwritten, so it's safe to call `go generate` many times
|
||||
* Use `$GOFILE` to refer to the current file
|
||||
* The `//go:generate` line will be removed from the output
|
||||
|
||||
To see a real example of how to use `genny` with `go generate`, look in the [example/go-generate directory](https://github.com/cheekybits/genny/tree/master/examples/go-generate).
|
||||
|
||||
## How it works
|
||||
|
||||
Define your generic types using the special `generic.Type` placeholder type:
|
||||
|
||||
```go
|
||||
type KeyType generic.Type
|
||||
type ValueType generic.Type
|
||||
```
|
||||
|
||||
* You can use as many as you like
|
||||
* Give them meaningful names
|
||||
|
||||
Then write the generic code referencing the types as your normally would:
|
||||
|
||||
```go
|
||||
func SetValueTypeForKeyType(key KeyType, value ValueType) { /* ... */ }
|
||||
```
|
||||
|
||||
* Generic type names will also be replaced in comments and function names (see Real example below)
|
||||
|
||||
Since `generic.Type` is a real Go type, your code will compile, and you can even write unit tests against your generic code.
|
||||
|
||||
#### Generating specific versions
|
||||
|
||||
Pass the file through the `genny gen` tool with the specific types as the argument:
|
||||
|
||||
```
|
||||
cat generic.go | genny gen "KeyType=string ValueType=interface{}"
|
||||
```
|
||||
|
||||
The output will be the complete Go source file with the generic types replaced with the types specified in the arguments.
|
||||
|
||||
## Real example
|
||||
|
||||
Given [this generic Go code](https://github.com/cheekybits/genny/tree/master/examples/queue) which compiles and is tested:
|
||||
|
||||
```go
|
||||
package queue
|
||||
|
||||
import "github.com/cheekybits/genny/generic"
|
||||
|
||||
// NOTE: this is how easy it is to define a generic type
|
||||
type Something generic.Type
|
||||
|
||||
// SomethingQueue is a queue of Somethings.
|
||||
type SomethingQueue struct {
|
||||
items []Something
|
||||
}
|
||||
|
||||
func NewSomethingQueue() *SomethingQueue {
|
||||
return &SomethingQueue{items: make([]Something, 0)}
|
||||
}
|
||||
func (q *SomethingQueue) Push(item Something) {
|
||||
q.items = append(q.items, item)
|
||||
}
|
||||
func (q *SomethingQueue) Pop() Something {
|
||||
item := q.items[0]
|
||||
q.items = q.items[1:]
|
||||
return item
|
||||
}
|
||||
```
|
||||
|
||||
When `genny gen` is invoked like this:
|
||||
|
||||
```
|
||||
cat source.go | genny gen "Something=string"
|
||||
```
|
||||
|
||||
It outputs:
|
||||
|
||||
```go
|
||||
// This file was automatically generated by genny.
|
||||
// Any changes will be lost if this file is regenerated.
|
||||
// see https://github.com/cheekybits/genny
|
||||
|
||||
package queue
|
||||
|
||||
// StringQueue is a queue of Strings.
|
||||
type StringQueue struct {
|
||||
items []string
|
||||
}
|
||||
|
||||
func NewStringQueue() *StringQueue {
|
||||
return &StringQueue{items: make([]string, 0)}
|
||||
}
|
||||
func (q *StringQueue) Push(item string) {
|
||||
q.items = append(q.items, item)
|
||||
}
|
||||
func (q *StringQueue) Pop() string {
|
||||
item := q.items[0]
|
||||
q.items = q.items[1:]
|
||||
return item
|
||||
}
|
||||
```
|
||||
|
||||
To get a _something_ for every built-in Go type plus one of your own types, you could run:
|
||||
|
||||
```
|
||||
cat source.go | genny gen "Something=BUILTINS,*MyType"
|
||||
```
|
||||
|
||||
#### More examples
|
||||
|
||||
Check out the [test code files](https://github.com/cheekybits/genny/tree/master/parse/test) for more real examples.
|
||||
|
||||
## Writing test code
|
||||
|
||||
Once you have defined a generic type with some code worth testing:
|
||||
|
||||
```go
|
||||
package slice
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/stretchr/gogen/generic"
|
||||
)
|
||||
|
||||
type MyType generic.Type
|
||||
|
||||
func EnsureMyTypeSlice(objectOrSlice interface{}) []MyType {
|
||||
log.Printf("%v", reflect.TypeOf(objectOrSlice))
|
||||
switch obj := objectOrSlice.(type) {
|
||||
case []MyType:
|
||||
log.Println(" returning it untouched")
|
||||
return obj
|
||||
case MyType:
|
||||
log.Println(" wrapping in slice")
|
||||
return []MyType{obj}
|
||||
default:
|
||||
panic("ensure slice needs MyType or []MyType")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can treat it like any normal Go type in your test code:
|
||||
|
||||
```go
|
||||
func TestEnsureMyTypeSlice(t *testing.T) {
|
||||
|
||||
myType := new(MyType)
|
||||
slice := EnsureMyTypeSlice(myType)
|
||||
if assert.NotNil(t, slice) {
|
||||
assert.Equal(t, slice[0], myType)
|
||||
}
|
||||
|
||||
slice = EnsureMyTypeSlice(slice)
|
||||
log.Printf("%#v", slice[0])
|
||||
if assert.NotNil(t, slice) {
|
||||
assert.Equal(t, slice[0], myType)
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Understanding what `generic.Type` is
|
||||
|
||||
Because `generic.Type` is an empty interface type (literally `interface{}`) every other type will be considered to be a `generic.Type` if you are switching on the type of an object. Of course, once the specific versions are generated, this issue goes away but it's worth knowing when you are writing your tests against generic code.
|
||||
|
||||
### Contributions
|
||||
|
||||
* See the [API documentation for the parse package](http://godoc.org/github.com/cheekybits/genny/parse)
|
||||
* Please do TDD
|
||||
* All input welcome
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
// Package main is the command line tool for Genny.
|
||||
package main
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/cheekybits/genny/parse"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
source | genny gen [-in=""] [-out=""] [-pkg=""] "KeyType=string,int ValueType=string,int"
|
||||
|
||||
*/
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
exitcodeInvalidArgs
|
||||
exitcodeInvalidTypeSet
|
||||
exitcodeStdinFailed
|
||||
exitcodeGenFailed
|
||||
exitcodeGetFailed
|
||||
exitcodeSourceFileInvalid
|
||||
exitcodeDestFileFailed
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
in = flag.String("in", "", "file to parse instead of stdin")
|
||||
out = flag.String("out", "", "file to save output to instead of stdout")
|
||||
pkgName = flag.String("pkg", "", "package name for generated files")
|
||||
prefix = "https://github.com/metabition/gennylib/raw/master/"
|
||||
)
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
if len(args) < 2 {
|
||||
usage()
|
||||
os.Exit(exitcodeInvalidArgs)
|
||||
}
|
||||
|
||||
if strings.ToLower(args[0]) != "gen" && strings.ToLower(args[0]) != "get" {
|
||||
usage()
|
||||
os.Exit(exitcodeInvalidArgs)
|
||||
}
|
||||
|
||||
// parse the typesets
|
||||
var setsArg = args[1]
|
||||
if strings.ToLower(args[0]) == "get" {
|
||||
setsArg = args[2]
|
||||
}
|
||||
typeSets, err := parse.TypeSet(setsArg)
|
||||
if err != nil {
|
||||
fatal(exitcodeInvalidTypeSet, err)
|
||||
}
|
||||
|
||||
var outWriter io.Writer
|
||||
if len(*out) > 0 {
|
||||
err := os.MkdirAll(path.Dir(*out), 0755)
|
||||
if err != nil {
|
||||
fatal(exitcodeDestFileFailed, err)
|
||||
}
|
||||
|
||||
outFile, err := os.Create(*out)
|
||||
if err != nil {
|
||||
fatal(exitcodeDestFileFailed, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
outWriter = outFile
|
||||
} else {
|
||||
outWriter = os.Stdout
|
||||
}
|
||||
|
||||
if strings.ToLower(args[0]) == "get" {
|
||||
if len(args) != 3 {
|
||||
fmt.Println("not enough arguments to get")
|
||||
usage()
|
||||
os.Exit(exitcodeInvalidArgs)
|
||||
}
|
||||
r, err := http.Get(prefix + args[1])
|
||||
if err != nil {
|
||||
fatal(exitcodeGetFailed, err)
|
||||
}
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
fatal(exitcodeGetFailed, err)
|
||||
}
|
||||
r.Body.Close()
|
||||
br := bytes.NewReader(b)
|
||||
err = gen(*in, *pkgName, br, typeSets, outWriter)
|
||||
} else if len(*in) > 0 {
|
||||
var file *os.File
|
||||
file, err = os.Open(*in)
|
||||
if err != nil {
|
||||
fatal(exitcodeSourceFileInvalid, err)
|
||||
}
|
||||
defer file.Close()
|
||||
err = gen(*in, *pkgName, file, typeSets, outWriter)
|
||||
} else {
|
||||
var source []byte
|
||||
source, err = ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fatal(exitcodeStdinFailed, err)
|
||||
}
|
||||
reader := bytes.NewReader(source)
|
||||
err = gen("stdin", *pkgName, reader, typeSets, outWriter)
|
||||
}
|
||||
|
||||
// do the work
|
||||
if err != nil {
|
||||
fatal(exitcodeGenFailed, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, `usage: genny [{flags}] gen "{types}"
|
||||
|
||||
gen - generates type specific code from generic code.
|
||||
get <package/file> - fetch a generic template from the online library and gen it.
|
||||
|
||||
{flags} - (optional) Command line flags (see below)
|
||||
{types} - (required) Specific types for each generic type in the source
|
||||
{types} format: {generic}={specific}[,another][ {generic2}={specific2}]
|
||||
|
||||
Examples:
|
||||
Generic=Specific
|
||||
Generic1=Specific1 Generic2=Specific2
|
||||
Generic1=Specific1,Specific2 Generic2=Specific3,Specific4
|
||||
|
||||
Flags:`)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func fatal(code int, a ...interface{}) {
|
||||
fmt.Println(a...)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// gen performs the generic generation.
|
||||
func gen(filename, pkgName string, in io.ReadSeeker, typesets []map[string]string, out io.Writer) error {
|
||||
|
||||
var output []byte
|
||||
var err error
|
||||
|
||||
output, err = parse.Generics(filename, pkgName, in, typesets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out.Write(output)
|
||||
return nil
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
package main_test
|
||||
-1
@@ -1 +0,0 @@
|
||||
language: go
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
# 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]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "6d212800a42e8ab5c146b8ace3490ee17e5225f9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/fatih/camelcase"
|
||||
packages = ["."]
|
||||
revision = "f6a740d52f961c60348ebb109adde9f4635d7540"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "d8ed2627bdf02c080bf22230dbb337003b7aba2d"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
||||
version = "v1.1.4"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
packages = ["."]
|
||||
revision = "7f0871f2e17818990e4eed73f9b5c2f429501228"
|
||||
version = "v2.2.4"
|
||||
|
||||
[[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 = "1879d7c016fcc8e210ca7fdeed4b099f4e7fc931d34ff47a170222c60eea8aab"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
|
||||
# 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/cheekybits/genny"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fatih/camelcase"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pkg/errors"
|
||||
version = "0.8.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.1.4"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
version = "2.2.4"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/coryb/yaml.v2"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/op/go-logging.v1"
|
||||
version = "1.0.0"
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
GENERATOR_SRC = \
|
||||
rawoption.go \
|
||||
$(NULL)
|
||||
|
||||
GENERATED_SRC = $(GENERATOR_SRC:%.go=gen-%.go)
|
||||
|
||||
test: $(GENERATED_SRC)
|
||||
go get -t -v
|
||||
go get github.com/kr/pretty
|
||||
go get gopkg.in/alecthomas/kingpin.v2
|
||||
go test
|
||||
|
||||
gen-%.go: %.go
|
||||
# use github.com/cheekybits/genny after https://github.com/cheekybits/genny/pull/42 is merged
|
||||
go get github.com/coryb/genny
|
||||
go generate
|
||||
|
||||
.PHONY: test
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
[](https://travis-ci.org/coryb/figtree)
|
||||
[](https://godoc.org/github.com/coryb/figtree)
|
||||
|
||||
Figtree is a go library to recusively parse and merge yaml based config files.
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptionsEnv(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
StringifyValue = true
|
||||
defer func() {
|
||||
StringifyValue = false
|
||||
}()
|
||||
|
||||
os.Clearenv()
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
got := []string{}
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, "FIGTREE_") {
|
||||
got = append(got, env)
|
||||
}
|
||||
}
|
||||
|
||||
sort.StringSlice(got).Sort()
|
||||
|
||||
expected := []string{
|
||||
"FIGTREE_ARRAY_1=[\"d1arr1val1\",\"d1arr1val2\",\"dupval\"]",
|
||||
"FIGTREE_BOOL_1=true",
|
||||
"FIGTREE_FLOAT_1=1.11",
|
||||
"FIGTREE_INT_1=111",
|
||||
"FIGTREE_MAP_1={\"dup\":\"d1dupval\",\"key0\":\"d1map1val0\",\"key1\":\"d1map1val1\"}",
|
||||
"FIGTREE_STRING_1=d1str1val1",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
|
||||
func TestOptionsNamedEnv(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
StringifyValue = true
|
||||
defer func() {
|
||||
StringifyValue = false
|
||||
}()
|
||||
|
||||
os.Clearenv()
|
||||
fig := NewFigTree()
|
||||
fig.EnvPrefix = "TEST"
|
||||
err := fig.LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
got := []string{}
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, "FIGTREE_") || strings.HasPrefix(env, "TEST_") {
|
||||
got = append(got, env)
|
||||
}
|
||||
}
|
||||
|
||||
sort.StringSlice(got).Sort()
|
||||
|
||||
expected := []string{
|
||||
"TEST_ARRAY_1=[\"d1arr1val1\",\"d1arr1val2\",\"dupval\"]",
|
||||
"TEST_BOOL_1=true",
|
||||
"TEST_FLOAT_1=1.11",
|
||||
"TEST_INT_1=111",
|
||||
"TEST_MAP_1={\"dup\":\"d1dupval\",\"key0\":\"d1map1val0\",\"key1\":\"d1map1val1\"}",
|
||||
"TEST_STRING_1=d1str1val1",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
|
||||
func TestBuiltinEnv(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
os.Clearenv()
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
got := []string{}
|
||||
for _, env := range os.Environ() {
|
||||
if strings.HasPrefix(env, "FIGTREE_") {
|
||||
got = append(got, env)
|
||||
}
|
||||
}
|
||||
|
||||
sort.StringSlice(got).Sort()
|
||||
|
||||
expected := []string{
|
||||
"FIGTREE_ARRAY_1=[\"d1arr1val1\",\"d1arr1val2\",\"dupval\"]",
|
||||
"FIGTREE_BOOL_1=true",
|
||||
"FIGTREE_FLOAT_1=1.11",
|
||||
"FIGTREE_INT_1=111",
|
||||
"FIGTREE_LEAVE_EMPTY=",
|
||||
"FIGTREE_MAP_1={\"dup\":\"d1dupval\",\"key0\":\"d1map1val0\",\"key1\":\"d1map1val1\"}",
|
||||
"FIGTREE_STRING_1=d1str1val1",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, got)
|
||||
}
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptionsExecConfigD3(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d3arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d3arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../exec.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../exec.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../../exec.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../../exec.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"exec.yml", true, "d3str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../../exec.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"../exec.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"exec.yml", true, "d3map1val2"},
|
||||
"key3": StringOption{"exec.yml", true, "d3map1val3"},
|
||||
},
|
||||
Int1: IntOption{"exec.yml", true, 333},
|
||||
Float1: Float32Option{"exec.yml", true, 3.33},
|
||||
Bool1: BoolOption{"exec.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsExecConfigD2(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../exec.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../exec.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"exec.yml", true, "d2str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../exec.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"exec.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"exec.yml", true, "d2map1val2"},
|
||||
},
|
||||
Int1: IntOption{"exec.yml", true, 222},
|
||||
Float1: Float32Option{"exec.yml", true, 2.22},
|
||||
Bool1: BoolOption{"exec.yml", true, false},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsExecConfigD1(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"exec.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"exec.yml", true, "d1str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"exec.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"exec.yml", true, "d1map1val1"},
|
||||
},
|
||||
Int1: IntOption{"exec.yml", true, 111},
|
||||
Float1: Float32Option{"exec.yml", true, 1.11},
|
||||
Bool1: BoolOption{"exec.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinExecConfigD3(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d3arr1val1")
|
||||
arr1 = append(arr1, "d3arr1val2")
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d3str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d2map1val1",
|
||||
"key2": "d3map1val2",
|
||||
"key3": "d3map1val3",
|
||||
},
|
||||
Int1: 333,
|
||||
Float1: 3.33,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinExecConfigD2(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d2str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d2map1val1",
|
||||
"key2": "d2map1val2",
|
||||
},
|
||||
Int1: 222,
|
||||
Float1: 2.22,
|
||||
// note this will be true from d1/exec.yml since the
|
||||
// d1/d2/exec.yml set it to false which is a zero value
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinExecConfigD1(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d1str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d1map1val1",
|
||||
},
|
||||
Int1: 111,
|
||||
Float1: 1.11,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("exec.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
+625
-129
@@ -10,6 +10,8 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@@ -17,69 +19,159 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
yaml "gopkg.in/coryb/yaml.v2"
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
)
|
||||
|
||||
var log = logging.MustGetLogger("figtree")
|
||||
|
||||
type FigTree struct {
|
||||
ConfigDir string
|
||||
Defaults interface{}
|
||||
EnvPrefix string
|
||||
stop bool
|
||||
type Logger interface {
|
||||
Debugf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
func NewFigTree() *FigTree {
|
||||
return &FigTree{
|
||||
EnvPrefix: "FIGTREE",
|
||||
type nullLogger struct{}
|
||||
|
||||
func (*nullLogger) Debugf(string, ...interface{}) {}
|
||||
|
||||
var Log Logger = &nullLogger{}
|
||||
|
||||
func defaultApplyChangeSet(changeSet map[string]*string) error {
|
||||
for k, v := range changeSet {
|
||||
if v != nil {
|
||||
os.Setenv(k, *v)
|
||||
} else {
|
||||
os.Unsetenv(k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Option func(*FigTree)
|
||||
|
||||
func WithHome(home string) Option {
|
||||
return func(f *FigTree) {
|
||||
f.home = home
|
||||
}
|
||||
}
|
||||
|
||||
func LoadAllConfigs(configFile string, options interface{}) error {
|
||||
return NewFigTree().LoadAllConfigs(configFile, options)
|
||||
func WithCwd(cwd string) Option {
|
||||
return func(f *FigTree) {
|
||||
f.workDir = cwd
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfig(configFile string, options interface{}) error {
|
||||
return NewFigTree().LoadConfig(configFile, options)
|
||||
func WithEnvPrefix(env string) Option {
|
||||
return func(f *FigTree) {
|
||||
f.envPrefix = env
|
||||
}
|
||||
}
|
||||
|
||||
func WithConfigDir(dir string) Option {
|
||||
return func(f *FigTree) {
|
||||
f.configDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
type ChangeSetFunc func(map[string]*string) error
|
||||
|
||||
func WithApplyChangeSet(apply ChangeSetFunc) Option {
|
||||
return func(f *FigTree) {
|
||||
f.applyChangeSet = apply
|
||||
}
|
||||
}
|
||||
|
||||
type PreProcessor func([]byte) ([]byte, error)
|
||||
|
||||
func WithPreProcessor(pp PreProcessor) Option {
|
||||
return func(f *FigTree) {
|
||||
f.preProcessor = pp
|
||||
}
|
||||
}
|
||||
|
||||
type FigTree struct {
|
||||
home string
|
||||
workDir string
|
||||
configDir string
|
||||
envPrefix string
|
||||
preProcessor PreProcessor
|
||||
stop bool
|
||||
applyChangeSet ChangeSetFunc
|
||||
}
|
||||
|
||||
func NewFigTree(opts ...Option) *FigTree {
|
||||
wd, _ := os.Getwd()
|
||||
fig := &FigTree{
|
||||
home: os.Getenv("HOME"),
|
||||
workDir: wd,
|
||||
envPrefix: "FIGTREE",
|
||||
applyChangeSet: defaultApplyChangeSet,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(fig)
|
||||
}
|
||||
return fig
|
||||
}
|
||||
|
||||
func (f *FigTree) WithHome(home string) {
|
||||
WithHome(home)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithCwd(cwd string) {
|
||||
WithCwd(cwd)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithEnvPrefix(env string) {
|
||||
WithEnvPrefix(env)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithConfigDir(dir string) {
|
||||
WithConfigDir(dir)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithPreProcessor(pp PreProcessor) {
|
||||
WithPreProcessor(pp)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithApplyChangeSet(apply ChangeSetFunc) {
|
||||
WithApplyChangeSet(apply)(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) WithIgnoreChangeSet() {
|
||||
WithApplyChangeSet(func(_ map[string]*string) error {
|
||||
return nil
|
||||
})(f)
|
||||
}
|
||||
|
||||
func (f *FigTree) Copy() *FigTree {
|
||||
cp := *f
|
||||
return &cp
|
||||
}
|
||||
|
||||
func (f *FigTree) LoadAllConfigs(configFile string, options interface{}) error {
|
||||
// reset from any previous config parsing runs
|
||||
f.stop = false
|
||||
|
||||
if f.ConfigDir != "" {
|
||||
configFile = path.Join(f.ConfigDir, configFile)
|
||||
if f.configDir != "" {
|
||||
configFile = path.Join(f.configDir, configFile)
|
||||
}
|
||||
|
||||
paths := FindParentPaths(configFile)
|
||||
paths := FindParentPaths(f.home, f.workDir, configFile)
|
||||
paths = append([]string{fmt.Sprintf("/etc/%s", configFile)}, paths...)
|
||||
|
||||
// iterate paths in reverse
|
||||
for i := len(paths) - 1; i >= 0; i-- {
|
||||
file := paths[i]
|
||||
err := f.LoadConfig(file, options)
|
||||
if err != nil {
|
||||
if err := f.LoadConfig(file, options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.stop {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// apply defaults at the end to set any undefined fields
|
||||
if f.Defaults != nil {
|
||||
m := &merger{sourceFile: "default"}
|
||||
m.mergeStructs(
|
||||
reflect.ValueOf(options),
|
||||
reflect.ValueOf(f.Defaults),
|
||||
)
|
||||
f.populateEnv(options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FigTree) LoadConfigBytes(config []byte, source string, options interface{}) (err error) {
|
||||
f.populateEnv(options)
|
||||
func (f *FigTree) LoadConfigBytes(config []byte, source string, options interface{}) error {
|
||||
if !reflect.ValueOf(options).IsValid() {
|
||||
return fmt.Errorf("options argument [%#v] is not valid", options)
|
||||
}
|
||||
|
||||
defer func(mapType, iface reflect.Type) {
|
||||
yaml.DefaultMapType = mapType
|
||||
@@ -89,7 +181,15 @@ func (f *FigTree) LoadConfigBytes(config []byte, source string, options interfac
|
||||
yaml.DefaultMapType = reflect.TypeOf(map[string]interface{}{})
|
||||
yaml.IfaceType = yaml.DefaultMapType.Elem()
|
||||
|
||||
m := &merger{sourceFile: source}
|
||||
var err error
|
||||
if f.preProcessor != nil {
|
||||
config, err = f.preProcessor(config)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed to process config file: %s", source)
|
||||
}
|
||||
}
|
||||
|
||||
m := NewMerger(WithSourceFile(source))
|
||||
type tmpOpts struct {
|
||||
Config ConfigOptions
|
||||
}
|
||||
@@ -98,13 +198,13 @@ func (f *FigTree) LoadConfigBytes(config []byte, source string, options interfac
|
||||
// look for config settings first
|
||||
err = yaml.Unmarshal(config, m)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Unable to parse %s", source))
|
||||
return errors.Wrapf(err, "Unable to parse %s", source)
|
||||
}
|
||||
|
||||
// then parse document into requested struct
|
||||
err = yaml.Unmarshal(config, tmp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("Unable to parse %s", source))
|
||||
return errors.Wrapf(err, "Unable to parse %s", source)
|
||||
}
|
||||
|
||||
m.setSource(reflect.ValueOf(tmp))
|
||||
@@ -112,40 +212,35 @@ func (f *FigTree) LoadConfigBytes(config []byte, source string, options interfac
|
||||
reflect.ValueOf(options),
|
||||
reflect.ValueOf(tmp),
|
||||
)
|
||||
f.populateEnv(options)
|
||||
changeSet := f.PopulateEnv(options)
|
||||
if m.Config.Stop {
|
||||
f.stop = true
|
||||
return nil
|
||||
return f.applyChangeSet(changeSet)
|
||||
}
|
||||
return nil
|
||||
return f.applyChangeSet(changeSet)
|
||||
}
|
||||
|
||||
func (f *FigTree) LoadConfig(file string, options interface{}) (err error) {
|
||||
basePath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(basePath, file)
|
||||
func (f *FigTree) LoadConfig(file string, options interface{}) error {
|
||||
rel, err := filepath.Rel(f.workDir, file)
|
||||
if err != nil {
|
||||
rel = file
|
||||
}
|
||||
|
||||
if stat, err := os.Stat(file); err == nil {
|
||||
if stat.Mode()&0111 == 0 {
|
||||
log.Debugf("Loading config %s", file)
|
||||
Log.Debugf("Loading config %s", file)
|
||||
if data, err := ioutil.ReadFile(file); err == nil {
|
||||
return f.LoadConfigBytes(data, rel, options)
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Found Executable Config file: %s", file)
|
||||
Log.Debugf("Found Executable Config file: %s", file)
|
||||
// it is executable, so run it and try to parse the output
|
||||
cmd := exec.Command(file)
|
||||
stdout := bytes.NewBufferString("")
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = bytes.NewBufferString("")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("%s is exectuable, but it failed to execute:\n%s", file, cmd.Stderr))
|
||||
return errors.Wrapf(err, "%s is exectuable, but it failed to execute:\n%s", file, cmd.Stderr)
|
||||
}
|
||||
return f.LoadConfigBytes(stdout.Bytes(), rel, options)
|
||||
}
|
||||
@@ -153,17 +248,261 @@ func (f *FigTree) LoadConfig(file string, options interface{}) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindParentPaths(homedir, cwd, fileName string) []string {
|
||||
paths := make([]string, 0)
|
||||
|
||||
// special case if homedir is not in current path then check there anyway
|
||||
if !strings.HasPrefix(cwd, homedir) {
|
||||
file := path.Join(homedir, fileName)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
paths = append(paths, filepath.FromSlash(file))
|
||||
}
|
||||
}
|
||||
|
||||
var dir string
|
||||
for _, part := range strings.Split(cwd, string(os.PathSeparator)) {
|
||||
if part == "" && dir == "" {
|
||||
dir = "/"
|
||||
} else {
|
||||
dir = path.Join(dir, part)
|
||||
}
|
||||
file := path.Join(dir, fileName)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
paths = append(paths, filepath.FromSlash(file))
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func (f *FigTree) FindParentPaths(fileName string) []string {
|
||||
return FindParentPaths(f.home, f.workDir, fileName)
|
||||
}
|
||||
|
||||
var camelCaseWords = regexp.MustCompile("[0-9A-Za-z]+")
|
||||
|
||||
func camelCase(name string) string {
|
||||
words := camelCaseWords.FindAllString(name, -1)
|
||||
for i, word := range words {
|
||||
words[i] = strings.Title(word)
|
||||
}
|
||||
return strings.Join(words, "")
|
||||
|
||||
}
|
||||
|
||||
type Merger struct {
|
||||
sourceFile string
|
||||
preserveMap map[string]struct{}
|
||||
Config ConfigOptions `json:"config,omitempty" yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
type MergeOption func(*Merger)
|
||||
|
||||
func WithSourceFile(source string) MergeOption {
|
||||
return func(m *Merger) {
|
||||
m.sourceFile = source
|
||||
}
|
||||
}
|
||||
|
||||
func PreserveMap(keys ...string) MergeOption {
|
||||
return func(m *Merger) {
|
||||
for _, key := range keys {
|
||||
m.preserveMap[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewMerger(options ...MergeOption) *Merger {
|
||||
m := &Merger{
|
||||
sourceFile: "merge",
|
||||
preserveMap: make(map[string]struct{}),
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Merge will attempt to merge the data from src into dst. They shoud be either both maps or both structs.
|
||||
// The structs do not need to have the same structure, but any field name that exists in both
|
||||
// structs will must be the same type.
|
||||
func Merge(dst, src interface{}) {
|
||||
m := NewMerger()
|
||||
m.mergeStructs(reflect.ValueOf(dst), reflect.ValueOf(src))
|
||||
}
|
||||
|
||||
// MakeMergeStruct will take multiple structs and return a pointer to a zero value for the
|
||||
// anonymous struct that has all the public fields from all the structs merged into one struct.
|
||||
// If there are multiple structs with the same field names, the first appearance of that name
|
||||
// will be used.
|
||||
func MakeMergeStruct(structs ...interface{}) interface{} {
|
||||
m := NewMerger()
|
||||
return m.MakeMergeStruct(structs...)
|
||||
}
|
||||
|
||||
func (m *Merger) MakeMergeStruct(structs ...interface{}) interface{} {
|
||||
values := []reflect.Value{}
|
||||
for _, data := range structs {
|
||||
values = append(values, reflect.ValueOf(data))
|
||||
}
|
||||
return m.makeMergeStruct(values...).Interface()
|
||||
}
|
||||
|
||||
func inlineField(field reflect.StructField) bool {
|
||||
if tag := field.Tag.Get("figtree"); tag != "" {
|
||||
return strings.HasSuffix(tag, ",inline")
|
||||
}
|
||||
if tag := field.Tag.Get("yaml"); tag != "" {
|
||||
return strings.HasSuffix(tag, ",inline")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Merger) makeMergeStruct(values ...reflect.Value) reflect.Value {
|
||||
foundFields := map[string]reflect.StructField{}
|
||||
for i := 0; i < len(values); i++ {
|
||||
v := values[i]
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
typ := v.Type()
|
||||
var field reflect.StructField
|
||||
if typ.Kind() == reflect.Struct {
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field = typ.Field(i)
|
||||
if field.PkgPath != "" {
|
||||
// unexported field, skip
|
||||
continue
|
||||
}
|
||||
if f, ok := foundFields[field.Name]; ok {
|
||||
if f.Type.Kind() == reflect.Struct && field.Type.Kind() == reflect.Struct {
|
||||
if fName, fieldName := f.Type.Name(), field.Type.Name(); fName == "" || fieldName == "" || fName != fieldName {
|
||||
// we have 2 fields with the same name and they are both structs, so we need
|
||||
// to merge the existing struct with the new one in case they are different
|
||||
newval := m.makeMergeStruct(reflect.New(f.Type).Elem(), reflect.New(field.Type).Elem()).Elem()
|
||||
f.Type = newval.Type()
|
||||
foundFields[field.Name] = f
|
||||
}
|
||||
}
|
||||
// field already found, skip
|
||||
continue
|
||||
}
|
||||
if inlineField(field) {
|
||||
values = append(values, v.Field(i))
|
||||
continue
|
||||
}
|
||||
foundFields[field.Name] = field
|
||||
}
|
||||
} else if typ.Kind() == reflect.Map {
|
||||
for _, key := range v.MapKeys() {
|
||||
keyval := reflect.ValueOf(v.MapIndex(key).Interface())
|
||||
if _, ok := m.preserveMap[key.String()]; !ok {
|
||||
if keyval.Kind() == reflect.Ptr && keyval.Elem().Kind() == reflect.Map {
|
||||
keyval = m.makeMergeStruct(keyval.Elem())
|
||||
} else if keyval.Kind() == reflect.Map {
|
||||
keyval = m.makeMergeStruct(keyval).Elem()
|
||||
}
|
||||
}
|
||||
var t reflect.Type
|
||||
if !keyval.IsValid() {
|
||||
// this nonsense is to create a generic `interface{}` type. There is
|
||||
// probably an easier to do this, but it eludes me at the moment.
|
||||
var dummy interface{}
|
||||
t = reflect.ValueOf(&dummy).Elem().Type()
|
||||
} else {
|
||||
t = reflect.ValueOf(keyval.Interface()).Type()
|
||||
}
|
||||
field = reflect.StructField{
|
||||
Name: camelCase(key.String()),
|
||||
Type: t,
|
||||
Tag: reflect.StructTag(fmt.Sprintf(`json:"%s" yaml:"%s"`, key.String(), key.String())),
|
||||
}
|
||||
if f, ok := foundFields[field.Name]; ok {
|
||||
if f.Type.Kind() == reflect.Struct && t.Kind() == reflect.Struct {
|
||||
if fName, tName := f.Type.Name(), t.Name(); fName == "" || tName == "" || fName != tName {
|
||||
// we have 2 fields with the same name and they are both structs, so we need
|
||||
// to merge the existig struct with the new one in case they are different
|
||||
newval := m.makeMergeStruct(reflect.New(f.Type).Elem(), reflect.New(t).Elem()).Elem()
|
||||
f.Type = newval.Type()
|
||||
foundFields[field.Name] = f
|
||||
}
|
||||
}
|
||||
// field already found, skip
|
||||
continue
|
||||
}
|
||||
foundFields[field.Name] = field
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields := []reflect.StructField{}
|
||||
for _, value := range foundFields {
|
||||
fields = append(fields, value)
|
||||
}
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
return fields[i].Name < fields[j].Name
|
||||
})
|
||||
newType := reflect.StructOf(fields)
|
||||
return reflect.New(newType)
|
||||
}
|
||||
|
||||
func (m *Merger) mapToStruct(src reflect.Value) reflect.Value {
|
||||
if src.Kind() != reflect.Map {
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
dest := m.makeMergeStruct(src)
|
||||
if dest.Kind() == reflect.Ptr {
|
||||
dest = dest.Elem()
|
||||
}
|
||||
|
||||
for _, key := range src.MapKeys() {
|
||||
structFieldName := camelCase(key.String())
|
||||
keyval := reflect.ValueOf(src.MapIndex(key).Interface())
|
||||
// skip invalid (ie nil) key values
|
||||
if !keyval.IsValid() {
|
||||
continue
|
||||
}
|
||||
if keyval.Kind() == reflect.Ptr && keyval.Elem().Kind() == reflect.Map {
|
||||
keyval = m.mapToStruct(keyval.Elem()).Addr()
|
||||
m.mergeStructs(dest.FieldByName(structFieldName), reflect.ValueOf(keyval.Interface()))
|
||||
} else if keyval.Kind() == reflect.Map {
|
||||
keyval = m.mapToStruct(keyval)
|
||||
m.mergeStructs(dest.FieldByName(structFieldName), reflect.ValueOf(keyval.Interface()))
|
||||
} else {
|
||||
dest.FieldByName(structFieldName).Set(reflect.ValueOf(keyval.Interface()))
|
||||
}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
func structToMap(src reflect.Value) reflect.Value {
|
||||
if src.Kind() != reflect.Struct {
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
dest := reflect.ValueOf(map[string]interface{}{})
|
||||
|
||||
typ := src.Type()
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
structField := typ.Field(i)
|
||||
if structField.PkgPath != "" {
|
||||
// skip private fields
|
||||
continue
|
||||
}
|
||||
name := yamlFieldName(structField)
|
||||
dest.SetMapIndex(reflect.ValueOf(name), src.Field(i))
|
||||
}
|
||||
|
||||
return dest
|
||||
}
|
||||
|
||||
type ConfigOptions struct {
|
||||
Overwrite []string `json:"overwrite,omitempty" yaml:"overwrite,omitempty"`
|
||||
Stop bool `json:"stop,omitempty" yaml:"stop,omitempty"`
|
||||
// Merge bool `json:"merge,omitempty" yaml:"merge,omitempty"`
|
||||
}
|
||||
|
||||
type merger struct {
|
||||
sourceFile string
|
||||
Config ConfigOptions `json:"config,omitempty" yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
func yamlFieldName(sf reflect.StructField) string {
|
||||
if tag, ok := sf.Tag.Lookup("yaml"); ok {
|
||||
// with yaml:"foobar,omitempty"
|
||||
@@ -171,10 +510,16 @@ func yamlFieldName(sf reflect.StructField) string {
|
||||
parts := strings.Split(tag, ",")
|
||||
return parts[0]
|
||||
}
|
||||
return sf.Name
|
||||
// guess the field name from reversing camel case
|
||||
// so "FooBar" becomes "foo-bar"
|
||||
parts := camelcase.Split(sf.Name)
|
||||
for i := range parts {
|
||||
parts[i] = strings.ToLower(parts[i])
|
||||
}
|
||||
return strings.Join(parts, "-")
|
||||
}
|
||||
|
||||
func (m *merger) mustOverwrite(name string) bool {
|
||||
func (m *Merger) mustOverwrite(name string) bool {
|
||||
for _, prop := range m.Config.Overwrite {
|
||||
if name == prop {
|
||||
return true
|
||||
@@ -185,7 +530,7 @@ func (m *merger) mustOverwrite(name string) bool {
|
||||
|
||||
func isDefault(v reflect.Value) bool {
|
||||
if v.CanAddr() {
|
||||
if option, ok := v.Addr().Interface().(Option); ok {
|
||||
if option, ok := v.Addr().Interface().(option); ok {
|
||||
if option.GetSource() == "default" {
|
||||
return true
|
||||
}
|
||||
@@ -194,7 +539,10 @@ func isDefault(v reflect.Value) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isEmpty(v reflect.Value) bool {
|
||||
func isZero(v reflect.Value) bool {
|
||||
if !v.IsValid() {
|
||||
return true
|
||||
}
|
||||
return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
|
||||
}
|
||||
|
||||
@@ -203,7 +551,7 @@ func isSame(v1, v2 reflect.Value) bool {
|
||||
}
|
||||
|
||||
// recursively set the Source attribute of the Options
|
||||
func (m *merger) setSource(v reflect.Value) {
|
||||
func (m *Merger) setSource(v reflect.Value) {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
@@ -222,7 +570,7 @@ func (m *merger) setSource(v reflect.Value) {
|
||||
}
|
||||
case reflect.Struct:
|
||||
if v.CanAddr() {
|
||||
if option, ok := v.Addr().Interface().(Option); ok {
|
||||
if option, ok := v.Addr().Interface().(option); ok {
|
||||
if option.IsDefined() {
|
||||
option.SetSource(m.sourceFile)
|
||||
}
|
||||
@@ -247,23 +595,117 @@ func (m *merger) setSource(v reflect.Value) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *merger) mergeStructs(ov, nv reflect.Value) {
|
||||
if ov.Kind() == reflect.Ptr {
|
||||
ov = ov.Elem()
|
||||
func (m *Merger) assignValue(dest, src reflect.Value, overwrite bool) {
|
||||
if src.Type().AssignableTo(dest.Type()) {
|
||||
if (isZero(dest) || isDefault(dest) || overwrite) && !isZero(src) {
|
||||
dest.Set(src)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if nv.Kind() == reflect.Ptr {
|
||||
nv = nv.Elem()
|
||||
if dest.CanAddr() {
|
||||
if option, ok := dest.Addr().Interface().(option); ok {
|
||||
destOptionValue := reflect.ValueOf(option.GetValue())
|
||||
// map interface type to real-ish type:
|
||||
src = reflect.ValueOf(src.Interface())
|
||||
if !src.IsValid() {
|
||||
Log.Debugf("assignValue: src isValid: %t", src.IsValid())
|
||||
return
|
||||
}
|
||||
if src.Type().AssignableTo(destOptionValue.Type()) {
|
||||
option.SetValue(src.Interface())
|
||||
option.SetSource(m.sourceFile)
|
||||
Log.Debugf("assignValue: assigned %#v to %#v", destOptionValue, src)
|
||||
return
|
||||
} else {
|
||||
panic(fmt.Errorf("%s is not assinable to %s", src.Type(), destOptionValue.Type()))
|
||||
}
|
||||
}
|
||||
}
|
||||
if ov.Kind() == reflect.Map && nv.Kind() == reflect.Map {
|
||||
// make copy so we can reliably Addr it to see if it fits the
|
||||
// Option interface.
|
||||
srcCopy := reflect.New(src.Type()).Elem()
|
||||
srcCopy.Set(src)
|
||||
if option, ok := srcCopy.Addr().Interface().(option); ok {
|
||||
srcOptionValue := reflect.ValueOf(option.GetValue())
|
||||
if srcOptionValue.Type().AssignableTo(dest.Type()) {
|
||||
m.assignValue(dest, srcOptionValue, overwrite)
|
||||
return
|
||||
} else {
|
||||
panic(fmt.Errorf("%s is not assinable to %s", srcOptionValue.Type(), dest.Type()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fromInterface(v reflect.Value) (reflect.Value, func()) {
|
||||
if v.Kind() == reflect.Interface {
|
||||
realV := reflect.ValueOf(v.Interface())
|
||||
if !realV.IsValid() {
|
||||
realV = reflect.New(v.Type()).Elem()
|
||||
v.Set(realV)
|
||||
return v, func() {}
|
||||
}
|
||||
tmp := reflect.New(realV.Type()).Elem()
|
||||
tmp.Set(realV)
|
||||
return tmp, func() {
|
||||
v.Set(tmp)
|
||||
}
|
||||
}
|
||||
return v, func() {}
|
||||
}
|
||||
|
||||
func (m *Merger) mergeStructs(ov, nv reflect.Value) {
|
||||
ov = reflect.Indirect(ov)
|
||||
nv = reflect.Indirect(nv)
|
||||
|
||||
ov, restore := fromInterface(ov)
|
||||
defer restore()
|
||||
|
||||
if nv.Kind() == reflect.Interface {
|
||||
nv = reflect.ValueOf(nv.Interface())
|
||||
}
|
||||
|
||||
if ov.Kind() == reflect.Map {
|
||||
if nv.Kind() == reflect.Struct {
|
||||
nv = structToMap(nv)
|
||||
}
|
||||
m.mergeMaps(ov, nv)
|
||||
return
|
||||
}
|
||||
|
||||
if ov.Kind() == reflect.Struct && nv.Kind() == reflect.Map {
|
||||
nv = m.mapToStruct(nv)
|
||||
}
|
||||
|
||||
if !ov.IsValid() || !nv.IsValid() {
|
||||
Log.Debugf("Valid: ov:%v nv:%t", ov.IsValid(), nv.IsValid())
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < nv.NumField(); i++ {
|
||||
ovStructField := ov.Type().Field(i)
|
||||
nvField := nv.Field(i)
|
||||
if nvField.Kind() == reflect.Interface {
|
||||
nvField = reflect.ValueOf(nvField.Interface())
|
||||
}
|
||||
if !nvField.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
nvStructField := nv.Type().Field(i)
|
||||
ovStructField, ok := ov.Type().FieldByName(nvStructField.Name)
|
||||
if !ok {
|
||||
if nvStructField.Anonymous {
|
||||
// this is an embedded struct, and the destination does not contain
|
||||
// the same embeded struct, so try to merge the embedded struct
|
||||
// directly with the destination
|
||||
m.mergeStructs(ov, nvField)
|
||||
continue
|
||||
}
|
||||
// if original value does not have the same struct field
|
||||
// then just skip this field.
|
||||
continue
|
||||
}
|
||||
|
||||
// PkgPath is empty for upper case (exported) field names.
|
||||
if ovStructField.PkgPath != "" || nvStructField.PkgPath != "" {
|
||||
// unexported field, skipping
|
||||
@@ -271,95 +713,133 @@ func (m *merger) mergeStructs(ov, nv reflect.Value) {
|
||||
}
|
||||
fieldName := yamlFieldName(ovStructField)
|
||||
|
||||
if (isEmpty(ov.Field(i)) || isDefault(ov.Field(i)) || m.mustOverwrite(fieldName)) && !isEmpty(nv.Field(i)) && !isSame(ov.Field(i), nv.Field(i)) {
|
||||
log.Debugf("Setting %s to %#v", nv.Type().Field(i).Name, nv.Field(i).Interface())
|
||||
ov.Field(i).Set(nv.Field(i))
|
||||
} else {
|
||||
switch ov.Field(i).Kind() {
|
||||
case reflect.Map:
|
||||
if nv.Field(i).Len() > 0 {
|
||||
log.Debugf("Merging: %v with %v", ov.Field(i), nv.Field(i))
|
||||
m.mergeMaps(ov.Field(i), nv.Field(i))
|
||||
}
|
||||
case reflect.Slice:
|
||||
if nv.Field(i).Len() > 0 {
|
||||
log.Debugf("Merging: %v with %v", ov.Field(i), nv.Field(i))
|
||||
if ov.Field(i).CanSet() {
|
||||
if ov.Field(i).Len() == 0 {
|
||||
ov.Field(i).Set(nv.Field(i))
|
||||
} else {
|
||||
log.Debugf("Merging: %v with %v", ov.Field(i), nv.Field(i))
|
||||
ov.Field(i).Set(m.mergeArrays(ov.Field(i), nv.Field(i)))
|
||||
}
|
||||
}
|
||||
ovField := ov.FieldByName(nvStructField.Name)
|
||||
ovField, restore := fromInterface(ovField)
|
||||
defer restore()
|
||||
|
||||
}
|
||||
case reflect.Array:
|
||||
if nv.Field(i).Len() > 0 {
|
||||
log.Debugf("Merging: %v with %v", ov.Field(i), nv.Field(i))
|
||||
ov.Field(i).Set(m.mergeArrays(ov.Field(i), nv.Field(i)))
|
||||
}
|
||||
case reflect.Struct:
|
||||
// only merge structs if they are not an Option type:
|
||||
if _, ok := ov.Field(i).Addr().Interface().(Option); !ok {
|
||||
log.Debugf("Merging: %v with %v", ov.Field(i), nv.Field(i))
|
||||
m.mergeStructs(ov.Field(i), nv.Field(i))
|
||||
}
|
||||
if (isZero(ovField) || isDefault(ovField) || m.mustOverwrite(fieldName)) && !isSame(ovField, nvField) {
|
||||
Log.Debugf("Setting %s to %#v", nv.Type().Field(i).Name, nvField.Interface())
|
||||
m.assignValue(ovField, nvField, m.mustOverwrite(fieldName))
|
||||
}
|
||||
switch ovField.Kind() {
|
||||
case reflect.Map:
|
||||
Log.Debugf("Merging Map: %#v with %#v", ovField, nvField)
|
||||
m.mergeStructs(ovField, nvField)
|
||||
case reflect.Slice:
|
||||
if nvField.Len() > 0 {
|
||||
Log.Debugf("Merging Slice: %#v with %#v", ovField, nvField)
|
||||
ovField.Set(m.mergeArrays(ovField, nvField))
|
||||
}
|
||||
case reflect.Array:
|
||||
if nvField.Len() > 0 {
|
||||
Log.Debugf("Merging Array: %v with %v", ovField, nvField)
|
||||
ovField.Set(m.mergeArrays(ovField, nvField))
|
||||
}
|
||||
case reflect.Struct:
|
||||
// only merge structs if they are not an Option type:
|
||||
if _, ok := ovField.Addr().Interface().(option); !ok {
|
||||
Log.Debugf("Merging Struct: %v with %v", ovField, nvField)
|
||||
m.mergeStructs(ovField, nvField)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *merger) mergeMaps(ov, nv reflect.Value) {
|
||||
func (m *Merger) mergeMaps(ov, nv reflect.Value) {
|
||||
for _, key := range nv.MapKeys() {
|
||||
if !ov.MapIndex(key).IsValid() {
|
||||
log.Debugf("Setting %v to %#v", key.Interface(), nv.MapIndex(key).Interface())
|
||||
ov.SetMapIndex(key, nv.MapIndex(key))
|
||||
ovElem := reflect.New(ov.Type().Elem()).Elem()
|
||||
m.assignValue(ovElem, nv.MapIndex(key), false)
|
||||
if ov.IsNil() {
|
||||
if !ov.CanSet() {
|
||||
continue
|
||||
}
|
||||
ov.Set(reflect.MakeMap(ov.Type()))
|
||||
}
|
||||
Log.Debugf("Setting %v to %#v", key.Interface(), ovElem.Interface())
|
||||
ov.SetMapIndex(key, ovElem)
|
||||
} else {
|
||||
ovi := reflect.ValueOf(ov.MapIndex(key).Interface())
|
||||
nvi := reflect.ValueOf(nv.MapIndex(key).Interface())
|
||||
if !nvi.IsValid() {
|
||||
continue
|
||||
}
|
||||
switch ovi.Kind() {
|
||||
case reflect.Map:
|
||||
log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
m.mergeMaps(ovi, nvi)
|
||||
Log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
m.mergeStructs(ovi, nvi)
|
||||
case reflect.Slice:
|
||||
log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
Log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
ov.SetMapIndex(key, m.mergeArrays(ovi, nvi))
|
||||
case reflect.Array:
|
||||
log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
Log.Debugf("Merging: %v with %v", ovi.Interface(), nvi.Interface())
|
||||
ov.SetMapIndex(key, m.mergeArrays(ovi, nvi))
|
||||
default:
|
||||
if isZero(ovi) {
|
||||
if !ovi.IsValid() || nvi.Type().AssignableTo(ovi.Type()) {
|
||||
ov.SetMapIndex(key, nvi)
|
||||
} else {
|
||||
// to check for the Option interface we need the Addr of the value, but
|
||||
// we cannot take the Addr of a map value, so we have to first copy
|
||||
// it, meh not optimal
|
||||
newVal := reflect.New(nvi.Type())
|
||||
newVal.Elem().Set(nvi)
|
||||
if nOption, ok := newVal.Interface().(option); ok {
|
||||
ov.SetMapIndex(key, reflect.ValueOf(nOption.GetValue()))
|
||||
continue
|
||||
}
|
||||
panic(fmt.Errorf("map value %T is not assignable to %T", nvi.Interface(), ovi.Interface()))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *merger) mergeArrays(ov, nv reflect.Value) reflect.Value {
|
||||
func (m *Merger) mergeArrays(ov, nv reflect.Value) reflect.Value {
|
||||
var zero interface{}
|
||||
Outer:
|
||||
for ni := 0; ni < nv.Len(); ni++ {
|
||||
niv := nv.Index(ni)
|
||||
|
||||
n := niv
|
||||
if n.CanAddr() {
|
||||
if nOption, ok := n.Addr().Interface().(option); ok {
|
||||
if !nOption.IsDefined() {
|
||||
continue
|
||||
}
|
||||
n = reflect.ValueOf(nOption.GetValue())
|
||||
}
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(n.Interface(), zero) {
|
||||
continue
|
||||
}
|
||||
|
||||
for oi := 0; oi < ov.Len(); oi++ {
|
||||
oiv := ov.Index(oi)
|
||||
if oiv.CanAddr() && niv.CanAddr() {
|
||||
if oOption, ok := oiv.Addr().Interface().(Option); ok {
|
||||
if nOption, ok := niv.Addr().Interface().(Option); ok {
|
||||
if reflect.DeepEqual(oOption.GetValue(), nOption.GetValue()) {
|
||||
continue Outer
|
||||
}
|
||||
}
|
||||
o := ov.Index(oi)
|
||||
if o.CanAddr() {
|
||||
if oOption, ok := o.Addr().Interface().(option); ok {
|
||||
o = reflect.ValueOf(oOption.GetValue())
|
||||
}
|
||||
}
|
||||
if reflect.DeepEqual(niv.Interface(), oiv.Interface()) {
|
||||
if reflect.DeepEqual(n.Interface(), o.Interface()) {
|
||||
continue Outer
|
||||
}
|
||||
}
|
||||
log.Debugf("Appending %v to %v", niv.Interface(), ov)
|
||||
ov = reflect.Append(ov, niv)
|
||||
|
||||
nvElem := reflect.New(ov.Type().Elem()).Elem()
|
||||
m.assignValue(nvElem, niv, false)
|
||||
|
||||
Log.Debugf("Appending %v to %v", nvElem.Interface(), ov)
|
||||
ov = reflect.Append(ov, nvElem)
|
||||
}
|
||||
return ov
|
||||
}
|
||||
|
||||
func (f *FigTree) formatEnvName(name string) string {
|
||||
name = fmt.Sprintf("%s_%s", f.EnvPrefix, strings.ToUpper(name))
|
||||
name = fmt.Sprintf("%s_%s", f.envPrefix, strings.ToUpper(name))
|
||||
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.IsDigit(r) || unicode.IsLetter(r) {
|
||||
@@ -412,7 +892,9 @@ func (f *FigTree) formatEnvValue(value reflect.Value) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (f *FigTree) populateEnv(data interface{}) {
|
||||
func (f *FigTree) PopulateEnv(data interface{}) (changeSet map[string]*string) {
|
||||
changeSet = make(map[string]*string)
|
||||
|
||||
options := reflect.ValueOf(data)
|
||||
if options.Kind() == reflect.Ptr {
|
||||
options = reflect.ValueOf(options.Elem().Interface())
|
||||
@@ -435,7 +917,9 @@ func (f *FigTree) populateEnv(data interface{}) {
|
||||
envName := f.formatEnvName(name)
|
||||
val, ok := f.formatEnvValue(options.MapIndex(key))
|
||||
if ok {
|
||||
os.Setenv(envName, val)
|
||||
changeSet[envName] = &val
|
||||
} else {
|
||||
changeSet[envName] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,28 +932,40 @@ func (f *FigTree) populateEnv(data interface{}) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.Join(camelcase.Split(structField.Name), "_")
|
||||
envNames := []string{strings.Join(camelcase.Split(structField.Name), "_")}
|
||||
|
||||
if tag := structField.Tag.Get("figtree"); tag != "" {
|
||||
if strings.HasSuffix(tag, ",inline") {
|
||||
// if we have a tag like: `figtree:",inline"` then we
|
||||
// want to the field as a top level member and not serialize
|
||||
// the raw struct to json, so just recurse here
|
||||
f.populateEnv(options.Field(i).Interface())
|
||||
nestedEnvSet := f.PopulateEnv(options.Field(i).Interface())
|
||||
for k, v := range nestedEnvSet {
|
||||
changeSet[k] = v
|
||||
}
|
||||
continue
|
||||
}
|
||||
// next look for `figtree:"env,..."` to set the env name to that
|
||||
parts := strings.Split(tag, ",")
|
||||
if len(parts) > 0 {
|
||||
name = parts[0]
|
||||
// if the env name is "-" then we should not populate this data into the env
|
||||
if parts[0] == "-" {
|
||||
continue
|
||||
}
|
||||
envNames = strings.Split(parts[0], ";")
|
||||
}
|
||||
}
|
||||
|
||||
envName := f.formatEnvName(name)
|
||||
val, ok := f.formatEnvValue(options.Field(i))
|
||||
if ok {
|
||||
os.Setenv(envName, val)
|
||||
for _, name := range envNames {
|
||||
envName := f.formatEnvName(name)
|
||||
val, ok := f.formatEnvValue(options.Field(i))
|
||||
if ok {
|
||||
changeSet[envName] = &val
|
||||
} else {
|
||||
changeSet[envName] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changeSet
|
||||
}
|
||||
|
||||
-282
@@ -1,282 +0,0 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
logging "gopkg.in/op/go-logging.v1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
StringifyValue = false
|
||||
logging.SetLevel(logging.NOTICE, "")
|
||||
}
|
||||
|
||||
type TestOptions struct {
|
||||
String1 StringOption `json:"str1,omitempty" yaml:"str1,omitempty"`
|
||||
LeaveEmpty StringOption `json:"leave-empty,omitempty" yaml:"leave-empty,omitempty"`
|
||||
Array1 ListStringOption `json:"arr1,omitempty" yaml:"arr1,omitempty"`
|
||||
Map1 MapStringOption `json:"map1,omitempty" yaml:"map1,omitempty"`
|
||||
Int1 IntOption `json:"int1,omitempty" yaml:"int1,omitempty"`
|
||||
Float1 Float32Option `json:"float1,omitempty" yaml:"float1,omitempty"`
|
||||
Bool1 BoolOption `json:"bool1,omitempty" yaml:"bool1,omitempty"`
|
||||
}
|
||||
|
||||
type TestBuiltin struct {
|
||||
String1 string `yaml:"str1,omitempty"`
|
||||
LeaveEmpty string `yaml:"leave-empty,omitempty"`
|
||||
Array1 []string `yaml:"arr1,omitempty"`
|
||||
Map1 map[string]string `yaml:"map1,omitempty"`
|
||||
Int1 int `yaml:"int1,omitempty"`
|
||||
Float1 float32 `yaml:"float1,omitempty"`
|
||||
Bool1 bool `yaml:"bool1,omitempty"`
|
||||
}
|
||||
|
||||
func TestOptionsLoadConfigD3(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d3arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d3arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "dupval"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../../figtree.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../../figtree.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"figtree.yml", true, "d3str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../../figtree.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"../figtree.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"figtree.yml", true, "d3map1val2"},
|
||||
"key3": StringOption{"figtree.yml", true, "d3map1val3"},
|
||||
"dup": StringOption{"figtree.yml", true, "d3dupval"},
|
||||
},
|
||||
Int1: IntOption{"figtree.yml", true, 333},
|
||||
Float1: Float32Option{"figtree.yml", true, 3.33},
|
||||
Bool1: BoolOption{"figtree.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsLoadConfigD2(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "dupval"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"figtree.yml", true, "d2str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../figtree.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"figtree.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"figtree.yml", true, "d2map1val2"},
|
||||
"dup": StringOption{"figtree.yml", true, "d2dupval"},
|
||||
},
|
||||
Int1: IntOption{"figtree.yml", true, 222},
|
||||
Float1: Float32Option{"figtree.yml", true, 2.22},
|
||||
Bool1: BoolOption{"figtree.yml", true, false},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsLoadConfigD1(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d1arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "dupval"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"figtree.yml", true, "d1str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"figtree.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"figtree.yml", true, "d1map1val1"},
|
||||
"dup": StringOption{"figtree.yml", true, "d1dupval"},
|
||||
},
|
||||
Int1: IntOption{"figtree.yml", true, 111},
|
||||
Float1: Float32Option{"figtree.yml", true, 1.11},
|
||||
Bool1: BoolOption{"figtree.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsCorrupt(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
err := LoadAllConfigs("corrupt.yml", &opts)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestBuiltinLoadConfigD3(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d3arr1val1")
|
||||
arr1 = append(arr1, "d3arr1val2")
|
||||
arr1 = append(arr1, "dupval")
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d3str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d2map1val1",
|
||||
"key2": "d3map1val2",
|
||||
"key3": "d3map1val3",
|
||||
"dup": "d3dupval",
|
||||
},
|
||||
Int1: 333,
|
||||
Float1: 3.33,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinLoadConfigD2(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "dupval")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d2str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d2map1val1",
|
||||
"key2": "d2map1val2",
|
||||
"dup": "d2dupval",
|
||||
},
|
||||
Int1: 222,
|
||||
Float1: 2.22,
|
||||
// note this will be true from d1/figtree.yml since the
|
||||
// d1/d2/figtree.yml set it to false which is a zero value
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinLoadConfigD1(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
arr1 = append(arr1, "dupval")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d1str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d1map1val1",
|
||||
"dup": "d1dupval",
|
||||
},
|
||||
Int1: 111,
|
||||
Float1: 1.11,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinCorrupt(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
err := LoadAllConfigs("corrupt.yml", &opts)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestOptionsLoadConfigDefaults(t *testing.T) {
|
||||
opts := TestOptions{
|
||||
String1: NewStringOption("defaultVal1"),
|
||||
LeaveEmpty: NewStringOption("emptyVal1"),
|
||||
Int1: NewIntOption(999),
|
||||
Float1: NewFloat32Option(9.99),
|
||||
Bool1: NewBoolOption(false),
|
||||
}
|
||||
os.Chdir("d1")
|
||||
defer os.Chdir("..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d1arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "dupval"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"figtree.yml", true, "d1str1val1"},
|
||||
LeaveEmpty: StringOption{"default", true, "emptyVal1"},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"figtree.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"figtree.yml", true, "d1map1val1"},
|
||||
"dup": StringOption{"figtree.yml", true, "d1dupval"},
|
||||
},
|
||||
Int1: IntOption{"figtree.yml", true, 111},
|
||||
Float1: Float32Option{"figtree.yml", true, 1.11},
|
||||
Bool1: BoolOption{"figtree.yml", true, true},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
+40
@@ -74,6 +74,7 @@ func (o *BoolOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -82,6 +83,7 @@ func (o *BoolOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -305,6 +307,7 @@ func (o *ByteOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -313,6 +316,7 @@ func (o *ByteOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -536,6 +540,7 @@ func (o *Complex128Option) UnmarshalYAML(unmarshal func(interface{}) error) erro
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -544,6 +549,7 @@ func (o *Complex128Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -767,6 +773,7 @@ func (o *Complex64Option) UnmarshalYAML(unmarshal func(interface{}) error) error
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -775,6 +782,7 @@ func (o *Complex64Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -998,6 +1006,7 @@ func (o *ErrorOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1006,6 +1015,7 @@ func (o *ErrorOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1229,6 +1239,7 @@ func (o *Float32Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1237,6 +1248,7 @@ func (o *Float32Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1460,6 +1472,7 @@ func (o *Float64Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1468,6 +1481,7 @@ func (o *Float64Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1691,6 +1705,7 @@ func (o *IntOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1699,6 +1714,7 @@ func (o *IntOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1922,6 +1938,7 @@ func (o *Int16Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -1930,6 +1947,7 @@ func (o *Int16Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2153,6 +2171,7 @@ func (o *Int32Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2161,6 +2180,7 @@ func (o *Int32Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2384,6 +2404,7 @@ func (o *Int64Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2392,6 +2413,7 @@ func (o *Int64Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2615,6 +2637,7 @@ func (o *Int8Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2623,6 +2646,7 @@ func (o *Int8Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2846,6 +2870,7 @@ func (o *RuneOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -2854,6 +2879,7 @@ func (o *RuneOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3077,6 +3103,7 @@ func (o *StringOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3085,6 +3112,7 @@ func (o *StringOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3308,6 +3336,7 @@ func (o *UintOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3316,6 +3345,7 @@ func (o *UintOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3539,6 +3569,7 @@ func (o *Uint16Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3547,6 +3578,7 @@ func (o *Uint16Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3770,6 +3802,7 @@ func (o *Uint32Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -3778,6 +3811,7 @@ func (o *Uint32Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4001,6 +4035,7 @@ func (o *Uint64Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4009,6 +4044,7 @@ func (o *Uint64Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4232,6 +4268,7 @@ func (o *Uint8Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4240,6 +4277,7 @@ func (o *Uint8Option) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4463,6 +4501,7 @@ func (o *UintptrOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -4471,6 +4510,7 @@ func (o *UintptrOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
func TestCommandLine(t *testing.T) {
|
||||
type CommandLineOptions struct {
|
||||
Str1 StringOption `yaml:"str1,omitempty"`
|
||||
Int1 IntOption `yaml:"int1,omitempty"`
|
||||
Map1 MapStringOption `yaml:"map1,omitempty"`
|
||||
Arr1 ListStringOption `yaml:"arr1,omitempty"`
|
||||
}
|
||||
|
||||
opts := CommandLineOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
app := kingpin.New("test", "testing")
|
||||
app.Flag("str1", "Str1").SetValue(&opts.Str1)
|
||||
app.Flag("int1", "Int1").SetValue(&opts.Int1)
|
||||
app.Flag("map1", "Map1").SetValue(&opts.Map1)
|
||||
app.Flag("arr1", "Arr1").SetValue(&opts.Arr1)
|
||||
_, err = app.Parse([]string{"--int1", "999", "--map1", "k1=v1", "--map1", "k2=v2", "--arr1", "v1", "--arr1", "v2"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
arr1 := ListStringOption{}
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d3arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "d3arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"figtree.yml", true, "dupval"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../figtree.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../../figtree.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../../figtree.yml", true, "d1arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"override", true, "v1"})
|
||||
arr1 = append(arr1, StringOption{"override", true, "v2"})
|
||||
|
||||
expected := CommandLineOptions{
|
||||
Str1: StringOption{"figtree.yml", true, "d3str1val1"},
|
||||
Int1: IntOption{"override", true, 999},
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../../figtree.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"../figtree.yml", true, "d2map1val1"},
|
||||
"key2": StringOption{"figtree.yml", true, "d3map1val2"},
|
||||
"key3": StringOption{"figtree.yml", true, "d3map1val3"},
|
||||
"dup": StringOption{"figtree.yml", true, "d3dupval"},
|
||||
"k1": StringOption{"override", true, "v1"},
|
||||
"k2": StringOption{"override", true, "v2"},
|
||||
},
|
||||
Arr1: arr1,
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, opts)
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
yaml "gopkg.in/coryb/yaml.v2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptionsMarshalYAML(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
StringifyValue = true
|
||||
defer func() {
|
||||
StringifyValue = false
|
||||
}()
|
||||
got, err := yaml.Marshal(&opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
expected := `str1: d3str1val1
|
||||
arr1:
|
||||
- d3arr1val1
|
||||
- d3arr1val2
|
||||
- dupval
|
||||
- d2arr1val1
|
||||
- d2arr1val2
|
||||
- d1arr1val1
|
||||
- d1arr1val2
|
||||
map1:
|
||||
dup: d3dupval
|
||||
key0: d1map1val0
|
||||
key1: d2map1val1
|
||||
key2: d3map1val2
|
||||
key3: d3map1val3
|
||||
int1: 333
|
||||
float1: 3.33
|
||||
bool1: true
|
||||
`
|
||||
assert.Equal(t, expected, string(got))
|
||||
}
|
||||
|
||||
func TestOptionsMarshalJSON(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
err := LoadAllConfigs("figtree.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
|
||||
StringifyValue = true
|
||||
defer func() {
|
||||
StringifyValue = false
|
||||
}()
|
||||
got, err := json.Marshal(&opts)
|
||||
assert.Nil(t, err)
|
||||
// note that "leave-empty" is serialized even though "omitempty" tag is set
|
||||
// this is because json always assumes structs are not empty and there
|
||||
// is no interface to override this behavior
|
||||
expected := `{"str1":"d3str1val1","leave-empty":"","arr1":["d3arr1val1","d3arr1val2","dupval","d2arr1val1","d2arr1val2","d1arr1val1","d1arr1val2"],"map1":{"dup":"d3dupval","key0":"d1map1val0","key1":"d2map1val1","key2":"d3map1val2","key3":"d3map1val3"},"int1":333,"float1":3.33,"bool1":true}`
|
||||
assert.Equal(t, expected, string(got))
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@ package figtree
|
||||
|
||||
import "regexp"
|
||||
|
||||
type Option interface {
|
||||
type option interface {
|
||||
IsDefined() bool
|
||||
GetValue() interface{}
|
||||
SetValue(interface{}) error
|
||||
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
StringifyValue = false
|
||||
}
|
||||
|
||||
func TestOptionsOverwriteConfigD3(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"../overwrite.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../overwrite.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../../overwrite.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../../overwrite.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"../overwrite.yml", true, "d2str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../../overwrite.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"../../overwrite.yml", true, "d1map1val1"},
|
||||
},
|
||||
Int1: IntOption{"../../overwrite.yml", true, 111},
|
||||
Float1: Float32Option{"../../overwrite.yml", true, 1.11},
|
||||
Bool1: BoolOption{"../overwrite.yml", true, false},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("overwrite.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestOptionsOverwriteConfigD2(t *testing.T) {
|
||||
opts := TestOptions{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []StringOption{}
|
||||
arr1 = append(arr1, StringOption{"overwrite.yml", true, "d2arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"overwrite.yml", true, "d2arr1val2"})
|
||||
arr1 = append(arr1, StringOption{"../overwrite.yml", true, "d1arr1val1"})
|
||||
arr1 = append(arr1, StringOption{"../overwrite.yml", true, "d1arr1val2"})
|
||||
|
||||
expected := TestOptions{
|
||||
String1: StringOption{"overwrite.yml", true, "d2str1val1"},
|
||||
LeaveEmpty: StringOption{},
|
||||
Array1: arr1,
|
||||
Map1: map[string]StringOption{
|
||||
"key0": StringOption{"../overwrite.yml", true, "d1map1val0"},
|
||||
"key1": StringOption{"../overwrite.yml", true, "d1map1val1"},
|
||||
},
|
||||
Int1: IntOption{"../overwrite.yml", true, 111},
|
||||
Float1: Float32Option{"../overwrite.yml", true, 1.11},
|
||||
Bool1: BoolOption{"overwrite.yml", true, false},
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("overwrite.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinOverwriteConfigD3(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2/d3")
|
||||
defer os.Chdir("../../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d2str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d1map1val1",
|
||||
},
|
||||
Int1: 111,
|
||||
Float1: 1.11,
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("overwrite.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
|
||||
func TestBuiltinOverwriteConfigD2(t *testing.T) {
|
||||
opts := TestBuiltin{}
|
||||
os.Chdir("d1/d2")
|
||||
defer os.Chdir("../..")
|
||||
|
||||
arr1 := []string{}
|
||||
arr1 = append(arr1, "d2arr1val1")
|
||||
arr1 = append(arr1, "d2arr1val2")
|
||||
arr1 = append(arr1, "d1arr1val1")
|
||||
arr1 = append(arr1, "d1arr1val2")
|
||||
|
||||
expected := TestBuiltin{
|
||||
String1: "d2str1val1",
|
||||
LeaveEmpty: "",
|
||||
Array1: arr1,
|
||||
Map1: map[string]string{
|
||||
"key0": "d1map1val0",
|
||||
"key1": "d1map1val1",
|
||||
},
|
||||
Int1: 111,
|
||||
Float1: 1.11,
|
||||
// note this will be true from d1/overwrite.yml since the
|
||||
// d1/d2/overwrite.yml set it to false which is a zero value
|
||||
Bool1: true,
|
||||
}
|
||||
|
||||
err := LoadAllConfigs("overwrite.yml", &opts)
|
||||
assert.Nil(t, err)
|
||||
assert.Exactly(t, expected, opts)
|
||||
}
|
||||
+2
@@ -76,6 +76,7 @@ func (o *RawTypeOption) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
if err := unmarshal(&o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "yaml"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
@@ -84,6 +85,7 @@ func (o *RawTypeOption) UnmarshalJSON(b []byte) error {
|
||||
if err := json.Unmarshal(b, &o.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Source = "json"
|
||||
o.Defined = true
|
||||
return nil
|
||||
}
|
||||
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
package figtree
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
yaml "gopkg.in/coryb/yaml.v2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOptionInterface(t *testing.T) {
|
||||
f := func(_ Option) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
assert.True(t, f(&BoolOption{}))
|
||||
assert.True(t, f(&ByteOption{}))
|
||||
assert.True(t, f(&Complex128Option{}))
|
||||
assert.True(t, f(&Complex64Option{}))
|
||||
assert.True(t, f(&ErrorOption{}))
|
||||
assert.True(t, f(&Float32Option{}))
|
||||
assert.True(t, f(&Float64Option{}))
|
||||
assert.True(t, f(&IntOption{}))
|
||||
assert.True(t, f(&Int16Option{}))
|
||||
assert.True(t, f(&Int32Option{}))
|
||||
assert.True(t, f(&Int64Option{}))
|
||||
assert.True(t, f(&Int8Option{}))
|
||||
assert.True(t, f(&RuneOption{}))
|
||||
assert.True(t, f(&StringOption{}))
|
||||
assert.True(t, f(&UintOption{}))
|
||||
assert.True(t, f(&Uint16Option{}))
|
||||
assert.True(t, f(&Uint32Option{}))
|
||||
assert.True(t, f(&Uint64Option{}))
|
||||
assert.True(t, f(&Uint8Option{}))
|
||||
assert.True(t, f(&UintptrOption{}))
|
||||
}
|
||||
|
||||
func TestStringOptionYAML(t *testing.T) {
|
||||
s := ""
|
||||
err := yaml.Unmarshal([]byte(`""`), &s)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, s, "")
|
||||
|
||||
type testType struct {
|
||||
String StringOption `yaml:"string,omitempty"`
|
||||
}
|
||||
tt := testType{}
|
||||
|
||||
err = yaml.Unmarshal([]byte(`string: ""`), &tt)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.String, StringOption{Value: "", Defined: true})
|
||||
|
||||
tt = testType{}
|
||||
err = yaml.Unmarshal([]byte(`string: "value"`), &tt)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.String, StringOption{Value: "value", Defined: true})
|
||||
}
|
||||
|
||||
func TestStringOptionJSON(t *testing.T) {
|
||||
type testType struct {
|
||||
String StringOption `json:"string,omitempty"`
|
||||
}
|
||||
tt := testType{}
|
||||
|
||||
err := json.Unmarshal([]byte(`{"string": ""}`), &tt)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.String, StringOption{Value: "", Defined: true})
|
||||
|
||||
tt = testType{}
|
||||
err = json.Unmarshal([]byte(`{"string": "value"}`), &tt)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tt.String, StringOption{Value: "value", Defined: true})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user