├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .vscode ├── cspell.json └── settings.json ├── LICENSE.txt ├── README.md ├── gh-label ├── go.mod ├── go.sum ├── internal ├── cmd │ ├── create │ │ ├── create.go │ │ └── create_test.go │ ├── delete │ │ ├── delete.go │ │ └── delete_test.go │ ├── edit │ │ ├── edit.go │ │ └── edit_test.go │ ├── export │ │ ├── export.go │ │ └── export_test.go │ ├── import │ │ ├── import.go │ │ └── import_test.go │ └── list │ │ ├── list.go │ │ └── list_test.go ├── github │ ├── cli.go │ ├── client.go │ ├── client_test.go │ ├── labels.go │ └── labels_test.go ├── options │ ├── options.go │ └── options_test.go └── utils │ ├── colors.go │ ├── colors_test.go │ ├── utils.go │ └── utils_test.go └── main.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [*.go] 8 | end_of_line = lf 9 | indent_style = tab 10 | 11 | [*.yml] 12 | indent_size = 2 13 | indent_style = space 14 | 15 | [gh-label] 16 | indent_size = 2 17 | indent_style = space 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.go text eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | env: 14 | GOVERSION: '1.17' 15 | 16 | jobs: 17 | test: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | include: 23 | - os: windows-latest 24 | goos: windows 25 | lint: true 26 | - os: ubuntu-latest 27 | goos: linux 28 | lint: true 29 | - os: macos-latest 30 | goos: darwin 31 | steps: 32 | - name: checkout 33 | uses: actions/checkout@v2 34 | - name: install go ${{ env.GOVERSION }} 35 | uses: actions/setup-go@v2.1.4 36 | with: 37 | go-version: ${{ env.GOVERSION }} 38 | - name: test 39 | run: GOOS=${{ matrix.goos }} go test ./... -cover -v 40 | - name: lint 41 | if: ${{ !cancelled() && matrix.lint }} 42 | uses: golangci/golangci-lint-action@v2.5.2 43 | with: 44 | only-new-issues: true 45 | skip-go-installation: true 46 | 47 | check: 48 | runs-on: ubuntu-latest 49 | if: ${{ !cancelled() }} 50 | needs: 51 | - test 52 | steps: 53 | - name: check 54 | run: | 55 | if [[ "${{ needs.test.result }}" == "success" ]]; then 56 | echo -e "\e[32mSuccess\e[0m" 57 | else 58 | echo -e "\e[31mFailed\e[0m" 59 | exit 1 60 | fi 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | env: 16 | GOVERSION: '1.17' 17 | 18 | jobs: 19 | publish: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: checkout 23 | uses: actions/checkout@v2 24 | - name: install go ${{ env.GOVERSION }} 25 | uses: actions/setup-go@v2.1.4 26 | with: 27 | go-version: ${{ env.GOVERSION }} 28 | - name: test 29 | run: go test ./... 30 | - name: publish 31 | uses: cli/gh-extension-precompile@v1 32 | with: 33 | go_version: ${{ env.GOVERSION }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | labels.csv 3 | labels.json 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # cSpell:disable 2 | 3 | linters: 4 | enable: 5 | - godot 6 | - gofmt 7 | - nilerr 8 | - stylecheck 9 | - whitespace 10 | 11 | linters-settings: 12 | godot: 13 | exclude: 14 | - cSpell 15 | -------------------------------------------------------------------------------- /.vscode/cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "words": [ 5 | "deserialization", 6 | "docf", 7 | "iostreams", 8 | "nolint", 9 | "vilmibm" 10 | ], 11 | "overrides": [ 12 | { 13 | "filename": "**/.github/**/{*.yml,*.yaml}", 14 | "words": [ 15 | "golangci", 16 | "goversion" 17 | ] 18 | }, 19 | { 20 | "filename": "**/*.md", 21 | "dictionaries": [ 22 | "golang" 23 | ] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[go]": { 3 | "editor.formatOnPaste": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Heath Stewart 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gh label 2 | 3 | [GitHub CLI] extension for issue label management. 4 | 5 | ## Install 6 | 7 | Make sure you have version 2.0 or [newer] of the [GitHub CLI] installed. 8 | 9 | ```bash 10 | gh extension install heaths/gh-label 11 | ``` 12 | 13 | ### Upgrade 14 | 15 | The `gh extension list` command shows if updates are available for extensions. To upgrade, you can use the `gh extension upgrade` command: 16 | 17 | ```bash 18 | gh extension upgrade heaths/gh-label 19 | 20 | # Or upgrade all extensions: 21 | gh extension upgrade --all 22 | ``` 23 | 24 | ## Commands 25 | 26 | ### create 27 | 28 | Create a label in a repository. 29 | You can specify colors with or without a preceeding hash ("#"). 30 | If you do not specify a color a random color will be choosen. 31 | 32 | ```bash 33 | gh label create feedback 34 | gh label create p1 --color e00808 35 | gh label create p2 --color "#ffa501" --description "Affects more than a few users" 36 | ``` 37 | 38 | ### delete 39 | 40 | Delete a label from a repository. 41 | 42 | ```bash 43 | gh label delete p1 44 | ``` 45 | 46 | ### edit 47 | 48 | Edit a label in a repository. 49 | You can specify colors with or without a preceeding hash ("#"). 50 | 51 | ```bash 52 | gh label edit general --new-name feedback 53 | gh label edit feedback --color c046ff --description "User feedback" 54 | ``` 55 | 56 | ### export 57 | 58 | Export labels from the repository to , or stdout if is "-". 59 | 60 | ```bash 61 | gh label export ./labels.csv 62 | gh label export ./labels.json 63 | gh label export --format csv - 64 | ``` 65 | 66 | ### import 67 | 68 | Import labels into the repository from , or stdin if is "-". 69 | 70 | ```bash 71 | gh label import ./labels.csv 72 | gh label import ./labels.json 73 | gh label import --format csv - 74 | ``` 75 | 76 | ### list 77 | 78 | List labels in a repository. 79 | You can optionally pass a substring to match in the label name or description. 80 | 81 | ```bash 82 | gh label list 83 | gh label list service 84 | ``` 85 | 86 | ## License 87 | 88 | Licensed under the [MIT](LICENSE.txt) license. 89 | 90 | Portions of this source copied from [vilmibm/gh-user-status](https://github.com/vilmibm/gh-user-status/tree/533285348c0354064d79053da39aa75f17b5c55f) under the [GNU Affero General Public License v3.0](https://github.com/vilmibm/gh-user-status/blob/533285348c0354064d79053da39aa75f17b5c55f/LICENSE). 91 | 92 | [GitHub CLI]: https://github.com/cli/cli 93 | [newer]: https://github.com/cli/cli/releases/latest 94 | -------------------------------------------------------------------------------- /gh-label: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Based on https://github.com/vilmibm/gh-user-status/blob/533285348c0354064d79053da39aa75f17b5c55f/gh-user-status 4 | 5 | set -e 6 | 7 | extensionPath="$(dirname "$0")" 8 | 9 | # Get the most recent tag, or a commit if no tags availble e.g. within a shallow clone. 10 | repo=heaths/gh-label 11 | tag="$(git --git-dir="${extensionPath}/.git" describe --abbrev=0 --always --tags)" 12 | 13 | arch="$(uname -m)" 14 | exec="" 15 | 16 | if uname -a | grep Msys > /dev/null; then 17 | if [ $arch = "x86_64" ]; then 18 | exe="windows-amd64.exe" 19 | elif [ $arch = "i686" ]; then 20 | exe="windows-386.exe" 21 | elif [ $arch = "i386" ]; then 22 | exe="windows-386.exe" 23 | fi 24 | elif uname -a | grep Darwin > /dev/null; then 25 | if [ $arch = "x86_64" ]; then 26 | exe="darwin-amd64" 27 | fi 28 | elif uname -a | grep Linux > /dev/null; then 29 | if [ $arch = "x86_64" ]; then 30 | exe="linux-amd64" 31 | elif [ $arch = "i686" ]; then 32 | exe="linux-386" 33 | elif [ $arch = "i386" ]; then 34 | exe="linux-386" 35 | fi 36 | fi 37 | 38 | if [ "${exe}" == "" ]; then 39 | if [ "$(which go)" == "" ]; then 40 | echo "Go must be installed to use this gh extension on this platform: https://golang.org" 41 | exit 1 42 | fi 43 | 44 | pushd "${extensionPath}" > /dev/null 45 | go run . "$@" 46 | popd > /dev/null 47 | 48 | exit 49 | fi 50 | 51 | if [[ ! -x "${extensionPath}/dist/${tag}/${exe}" ]]; then 52 | rm -rf "${extensionPath}"/dist/* 53 | 54 | if [[ $tag =~ ^v[0-9\.]+ ]]; then 55 | >&2 echo "Downloading ${exe} from https://github.com/${repo}/releases/tag/${tag}" 56 | gh release -R "${repo}" download "${tag}" -p "${exe}" --dir="${extensionPath}/dist/${tag}" 57 | else 58 | # Fall back to latest executable since no tag was available. 59 | >&2 echo "Downloading latest executable from https://github.com/${repo}/releases/latest" 60 | gh release -R "${repo}" download -p "${exe}" --dir="${extensionPath}/dist/${tag}" 61 | fi 62 | 63 | chmod +x "${extensionPath}/dist/${tag}/${exe}" 64 | fi 65 | 66 | exec "${extensionPath}/dist/${tag}/${exe}" "$@" 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/heaths/gh-label 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/MakeNowJust/heredoc v1.0.0 7 | github.com/cli/cli v1.14.1-0.20210823190025-e2973453b5cd 8 | github.com/cli/safeexec v1.0.0 9 | github.com/spf13/cobra v1.2.1 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 17 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 18 | cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= 19 | cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= 20 | cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= 21 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 22 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 23 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 24 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 25 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 26 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 27 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 28 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 29 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 30 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 31 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 32 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 33 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 34 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 35 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 36 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 37 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 38 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 39 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 40 | github.com/AlecAivazis/survey/v2 v2.3.1/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= 41 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 42 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 43 | github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= 44 | github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 45 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= 46 | github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= 47 | github.com/alecthomas/chroma v0.8.2/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= 48 | github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= 49 | github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= 50 | github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= 51 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 52 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 53 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 54 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 55 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 56 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 57 | github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= 58 | github.com/briandowns/spinner v1.11.1 h1:OixPqDEcX3juo5AjQZAnFPbeUA0jvkp2qzB5gOZJ/L0= 59 | github.com/briandowns/spinner v1.11.1/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= 60 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 61 | github.com/charmbracelet/glamour v0.3.0/go.mod h1:TzF0koPZhqq0YVBNL100cPHznAAjVj7fksX2RInwjGw= 62 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 63 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 64 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 65 | github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= 66 | github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI= 67 | github.com/cli/cli v1.14.1-0.20210823190025-e2973453b5cd h1:tQMzDNISl8vKj57pbOBiGw3/TTBAtVTA2rfFP1LXcsY= 68 | github.com/cli/cli v1.14.1-0.20210823190025-e2973453b5cd/go.mod h1:Ufn+Vb7QCrXwiZykwOU/OvaOxEY1U3msOP64wEvixbc= 69 | github.com/cli/oauth v0.8.0/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4= 70 | github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= 71 | github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= 72 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 73 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 74 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 75 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 76 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 77 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 78 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 79 | github.com/creack/pty v1.1.13/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 80 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= 81 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 82 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 83 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 84 | github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 85 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 86 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 87 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 88 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 89 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 90 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 91 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 92 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 93 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 94 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 95 | github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To= 96 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 97 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 98 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 99 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 100 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 101 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 102 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 103 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 104 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 105 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 106 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 107 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 108 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 109 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 110 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 111 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 112 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 113 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 114 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 115 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 116 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 117 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 118 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 119 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 120 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 121 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 122 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 123 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 124 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 125 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 126 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 127 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 128 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 129 | github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= 130 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 131 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 132 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 133 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 134 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 135 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 136 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 137 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 138 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 139 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 140 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 141 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 142 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 143 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 144 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 145 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 146 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 147 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 148 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 149 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 150 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 151 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 152 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 153 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 154 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 155 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 156 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 157 | github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 158 | github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 159 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 160 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 161 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 162 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 163 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 164 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 165 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 166 | github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= 167 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 168 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 169 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 170 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 171 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 172 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 173 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 174 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 175 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 176 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 177 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 178 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 179 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 180 | github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 181 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 182 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 183 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 184 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 185 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 186 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 187 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 188 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 189 | github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= 190 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= 191 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 192 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 193 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 194 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 195 | github.com/itchyny/go-flags v1.5.0/go.mod h1:lenkYuCobuxLBAd/HGFE4LRoW8D3B6iXRQfWYJ+MNbA= 196 | github.com/itchyny/gojq v0.12.4/go.mod h1:EQUSKgW/YaOxmXpAwGiowFDO4i2Rmtk5+9dFyeiymAg= 197 | github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= 198 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 199 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 200 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 201 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 202 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 203 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 204 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 205 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 206 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 207 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 208 | github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 209 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 210 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 211 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 212 | github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 213 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 214 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 215 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 216 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 217 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 218 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 219 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 220 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 221 | github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= 222 | github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 223 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 224 | github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= 225 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 226 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 227 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 228 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 229 | github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI= 230 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 231 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 232 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 233 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 234 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 235 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 236 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 237 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 238 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 239 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 240 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 241 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 242 | github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8= 243 | github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5 h1:T+Fc6qGlSfM+z0JPlp+n5rijvlg6C6JYFSNaqnCifDU= 244 | github.com/muesli/reflow v0.2.1-0.20210502190812-c80126ec2ad5/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= 245 | github.com/muesli/termenv v0.8.1 h1:9q230czSP3DHVpkaPDXGp0TOfAwyjyYwXlUCQxQSaBk= 246 | github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0= 247 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 248 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 249 | github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 250 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 251 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 252 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 253 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 254 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 255 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 256 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 257 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 258 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 259 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 260 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 261 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 262 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 263 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 264 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 265 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 266 | github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= 267 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= 268 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 269 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 270 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 271 | github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 272 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 273 | github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= 274 | github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= 275 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 276 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 277 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 278 | github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= 279 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 280 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 281 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 282 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 283 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 284 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 285 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 286 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 287 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 288 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 289 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 290 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 291 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 292 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 293 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 294 | github.com/yuin/goldmark v1.3.3/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 295 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 296 | github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= 297 | go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= 298 | go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= 299 | go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= 300 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 301 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 302 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 303 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 304 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 305 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 306 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 307 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 308 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 309 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 310 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 311 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 312 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 313 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 314 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 315 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 316 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 317 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 318 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 319 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 320 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 321 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 322 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 323 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 324 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 325 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 326 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 327 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 328 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 329 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 330 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 331 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 332 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 333 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 334 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 335 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 336 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 337 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 338 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 339 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 340 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 341 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 342 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 343 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 344 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 345 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 346 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 347 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 348 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 349 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 350 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 351 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 352 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 353 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 354 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 355 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 356 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 357 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 358 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 359 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 360 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 361 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 362 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 363 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 364 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 365 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 366 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 367 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 368 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 369 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 370 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 371 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 372 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 373 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 374 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 375 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 376 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 377 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 378 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 379 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 380 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 381 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 382 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 383 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 384 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 385 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 386 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 387 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 388 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= 389 | golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 390 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 391 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 392 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 393 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 394 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 395 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 396 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 397 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 398 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 399 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 400 | golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 401 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 402 | golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 403 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 404 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 405 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 406 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 407 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 408 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 409 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 410 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 411 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 412 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 413 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 414 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 415 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 416 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 417 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 418 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 419 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 420 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 421 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 422 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 426 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 427 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 428 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 429 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 430 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 431 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 432 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 433 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 434 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 435 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 436 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 437 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 438 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 439 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 440 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 441 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 442 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 443 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 444 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 445 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 448 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 449 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 450 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 451 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 452 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 453 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 454 | golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 455 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 456 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 457 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 458 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 459 | golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b h1:qh4f65QIVFjq9eBURLEYWqaEXmOyqdUyiBSgaXWccWk= 460 | golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 461 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 462 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= 463 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= 464 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 465 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 466 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 467 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 468 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 469 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 470 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 471 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 472 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 473 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 474 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 475 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 476 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 477 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 478 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 479 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 480 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 481 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 482 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 483 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 484 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 485 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 486 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 487 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 488 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 489 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 490 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 491 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 492 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 493 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 494 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 495 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 496 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 497 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 498 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 499 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 500 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 501 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 502 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 503 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 504 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 505 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 506 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 507 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 508 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 509 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 510 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 511 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 512 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 513 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 514 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 515 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 516 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 517 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 518 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 519 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 520 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 521 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 522 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 523 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 524 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 525 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 526 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 527 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 528 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 529 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 530 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 531 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 532 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 533 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 534 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 535 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 536 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 537 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 538 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 539 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 540 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 541 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 542 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 543 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 544 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 545 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 546 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 547 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 548 | google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= 549 | google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= 550 | google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= 551 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 552 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 553 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 554 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 555 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 556 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 557 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 558 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 559 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 560 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 561 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 562 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 563 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 564 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 565 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 566 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 567 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 568 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 569 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 570 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 571 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 572 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 573 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 574 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 575 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 576 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 577 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 578 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 579 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 580 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 581 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 582 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 583 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 584 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 585 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 586 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 587 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 588 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 589 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 590 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 591 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 592 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 593 | google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 594 | google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 595 | google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 596 | google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 597 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= 598 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 599 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 600 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 601 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 602 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 603 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 604 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 605 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 606 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 607 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 608 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 609 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 610 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 611 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 612 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 613 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 614 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 615 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 616 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 617 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 618 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 619 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 620 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 621 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 622 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 623 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 624 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 625 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 626 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 627 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 628 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 629 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 630 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 631 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 632 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 633 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 634 | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 635 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 636 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 637 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 638 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 639 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 640 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 641 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 642 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 643 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 644 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 645 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 646 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 647 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 648 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 649 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 650 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 651 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 652 | -------------------------------------------------------------------------------- /internal/cmd/create/create.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/MakeNowJust/heredoc" 8 | "github.com/cli/cli/pkg/iostreams" 9 | "github.com/heaths/gh-label/internal/github" 10 | "github.com/heaths/gh-label/internal/options" 11 | "github.com/heaths/gh-label/internal/utils" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type createOptions struct { 16 | name string 17 | color string 18 | description string 19 | 20 | // test 21 | client *github.Client 22 | io *iostreams.IOStreams 23 | } 24 | 25 | func CreateCmd(globalOpts *options.GlobalOptions) *cobra.Command { 26 | opts := &createOptions{} 27 | cmd := &cobra.Command{ 28 | Use: "create ", 29 | Short: "Create the label in the repository", 30 | Example: heredoc.Doc(` 31 | $ gh label create feedback 32 | $ gh label create p1 --color e00808 33 | $ gh label create p2 --color "#ffa501" --description "Affects more than a few users" 34 | `), 35 | Args: cobra.ExactArgs(1), 36 | PreRunE: func(cmd *cobra.Command, args []string) error { 37 | if opts.color != "" { 38 | if color, err := utils.ValidateColor(opts.color); err != nil { 39 | return fmt.Errorf(`invalid flag "color": %s`, err) 40 | } else { 41 | // Set color without "#" prefix. 42 | opts.color = color 43 | } 44 | } 45 | 46 | return nil 47 | }, 48 | RunE: func(cmd *cobra.Command, args []string) error { 49 | opts.name = args[0] 50 | 51 | return create(globalOpts, opts) 52 | }, 53 | } 54 | 55 | cmd.Flags().StringVarP(&opts.color, "color", "c", "", `The color of the label with or without "#" prefix. A random color will be assigned if not specified.`) 56 | cmd.Flags().StringVarP(&opts.description, "description", "d", "", "Description of the label.") 57 | 58 | return cmd 59 | } 60 | 61 | func create(globalOpts *options.GlobalOptions, opts *createOptions) error { 62 | if opts.client == nil { 63 | owner, repo := globalOpts.Repo() 64 | cli := &github.Cli{ 65 | Owner: owner, 66 | Repo: repo, 67 | } 68 | opts.client = github.New(cli) 69 | } 70 | 71 | if opts.io == nil { 72 | opts.io = iostreams.System() 73 | } 74 | 75 | if opts.color == "" { 76 | opts.color = utils.RandomColor() 77 | } 78 | 79 | label := github.Label{ 80 | Name: opts.name, 81 | Color: opts.color, 82 | Description: opts.description, 83 | } 84 | 85 | label, err := opts.client.CreateLabel(label) 86 | if err != nil { 87 | return fmt.Errorf("failed to create label; error: %w", err) 88 | } 89 | 90 | re := regexp.MustCompile("^https://api.([^/]+)/repos/(.*)$") 91 | matches := re.FindStringSubmatch(label.URL) 92 | 93 | if opts.io.IsStdoutTTY() { 94 | fmt.Fprintf(opts.io.Out, "Created label '%s'\n\n", label.Name) 95 | } 96 | 97 | if len(matches) == 3 { 98 | fmt.Fprintf(opts.io.Out, "https://%s/%s\n", matches[1], matches[2]) 99 | } else { 100 | fmt.Fprintln(opts.io.Out, label.URL) 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/cmd/create/create_test.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/MakeNowJust/heredoc" 9 | "github.com/cli/cli/pkg/iostreams" 10 | "github.com/heaths/gh-label/internal/github" 11 | "github.com/heaths/gh-label/internal/options" 12 | ) 13 | 14 | func Test_create(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | tty bool 18 | want string 19 | }{ 20 | { 21 | name: "create", 22 | want: "https://github.com/heaths/gh-label/labels/test\n", 23 | }, 24 | { 25 | name: "create (TTY)", 26 | tty: true, 27 | want: heredoc.Doc(`Created label 'test' 28 | 29 | https://github.com/heaths/gh-label/labels/test 30 | `), 31 | }, 32 | } 33 | 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | // Set up output streams. 37 | io, _, stdout, _ := iostreams.Test() 38 | io.SetStdoutTTY(tt.tty) 39 | io.SetColorEnabled(true) 40 | 41 | // Set up gh output. 42 | mock := &github.Mock{ 43 | Stdout: *bytes.NewBufferString(heredoc.Doc(`{ 44 | "id": 3315930645, 45 | "node_id": "MDU6TGFiZWwzMzE1OTMwNjQ1", 46 | "url": "https://api.github.com/repos/heaths/gh-label/labels/test", 47 | "name": "test", 48 | "color": "112233", 49 | "default": false, 50 | "description": "" 51 | }`)), 52 | } 53 | 54 | rootOpts := &options.GlobalOptions{} 55 | opts := &createOptions{ 56 | name: "test", 57 | color: "112233", 58 | 59 | client: github.New(mock), 60 | io: io, 61 | } 62 | 63 | if err := create(rootOpts, opts); err != nil { 64 | t.Errorf("create() error = %v", err) 65 | return 66 | } 67 | 68 | if gotW := stdout.String(); gotW != tt.want { 69 | t.Errorf("create() = %q, want %q", gotW, tt.want) 70 | } 71 | }) 72 | } 73 | } 74 | 75 | func Test_create_randomColor(t *testing.T) { 76 | t.Run("create with random color (TTY)", func(t *testing.T) { 77 | // Set up output streams. 78 | io, _, _, _ := iostreams.Test() 79 | io.SetStdoutTTY(true) 80 | io.SetColorEnabled(true) 81 | 82 | // Set up gh output. 83 | mock := &github.Mock{ 84 | Stdout: *bytes.NewBufferString(heredoc.Doc(`{ 85 | "id": 3315930645, 86 | "node_id": "MDU6TGFiZWwzMzE1OTMwNjQ1", 87 | "url": "https://api.github.com/repos/heaths/gh-label/labels/test", 88 | "name": "test", 89 | "color": "112233", 90 | "default": false, 91 | "description": "" 92 | }`)), 93 | } 94 | 95 | rootOpts := &options.GlobalOptions{} 96 | opts := &createOptions{ 97 | name: "test", 98 | 99 | client: github.New(mock), 100 | io: io, 101 | } 102 | 103 | if err := create(rootOpts, opts); err != nil { 104 | t.Errorf("create() error = %v", err) 105 | return 106 | } 107 | 108 | re := regexp.MustCompile("^[A-Z0-9]{6}$") 109 | if !re.MatchString(opts.color) { 110 | t.Errorf("expected random color pattern: %s, got color: %s", re.String(), opts.color) 111 | } 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /internal/cmd/delete/delete.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/MakeNowJust/heredoc" 7 | "github.com/cli/cli/pkg/iostreams" 8 | "github.com/heaths/gh-label/internal/github" 9 | "github.com/heaths/gh-label/internal/options" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type deleteOptions struct { 14 | name string 15 | 16 | // test 17 | client *github.Client 18 | io *iostreams.IOStreams 19 | } 20 | 21 | func DeleteCmd(globalOpts *options.GlobalOptions) *cobra.Command { 22 | opts := &deleteOptions{} 23 | cmd := &cobra.Command{ 24 | Use: "delete ", 25 | Short: "Delete the label from the repository", 26 | Example: heredoc.Doc(` 27 | $ gh label delete p1 28 | `), 29 | Args: cobra.ExactArgs(1), 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | opts.name = args[0] 32 | 33 | return delete(globalOpts, opts) 34 | }, 35 | } 36 | 37 | return cmd 38 | } 39 | 40 | func delete(globalOpts *options.GlobalOptions, opts *deleteOptions) error { 41 | if opts.client == nil { 42 | owner, repo := globalOpts.Repo() 43 | cli := &github.Cli{ 44 | Owner: owner, 45 | Repo: repo, 46 | } 47 | opts.client = github.New(cli) 48 | } 49 | 50 | if opts.io == nil { 51 | opts.io = iostreams.System() 52 | } 53 | 54 | if err := opts.client.DeleteLabel(opts.name); err != nil { 55 | return fmt.Errorf("failed to delete label: %w", err) 56 | } 57 | 58 | if opts.io.IsStdoutTTY() { 59 | fmt.Fprintf(opts.io.Out, "Deleted label '%s'\n", opts.name) 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/cmd/delete/delete_test.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/cli/cli/pkg/iostreams" 8 | "github.com/heaths/gh-label/internal/github" 9 | "github.com/heaths/gh-label/internal/options" 10 | ) 11 | 12 | func Test_delete(t *testing.T) { 13 | t.Run("delete (TTY)", func(t *testing.T) { 14 | // Set up output streams. 15 | io, _, stdout, _ := iostreams.Test() 16 | io.SetStdoutTTY(true) 17 | io.SetColorEnabled(true) 18 | 19 | // Set up gh output. 20 | mock := &github.Mock{} 21 | 22 | rootOpts := &options.GlobalOptions{} 23 | opts := &deleteOptions{ 24 | name: "test", 25 | 26 | client: github.New(mock), 27 | io: io, 28 | } 29 | 30 | if err := delete(rootOpts, opts); err != nil { 31 | t.Errorf("create() error = %v", err) 32 | return 33 | } 34 | 35 | want := "Deleted label 'test'\n" 36 | if want != stdout.String() { 37 | t.Errorf("create() = %s, want: %s", stdout.String(), want) 38 | } 39 | }) 40 | } 41 | 42 | func Test_delete_error(t *testing.T) { 43 | t.Run("delete with error", func(t *testing.T) { 44 | // Set up output streams. 45 | io, _, _, _ := iostreams.Test() 46 | io.SetStdoutTTY(true) 47 | io.SetColorEnabled(true) 48 | 49 | // Set up gh output. 50 | mock := &github.Mock{ 51 | Err: errors.New("gh returned error: exit status 1, stderr: gh: Not Found (HTTP 404)"), 52 | } 53 | 54 | rootOpts := &options.GlobalOptions{} 55 | opts := &deleteOptions{ 56 | name: "test", 57 | 58 | client: github.New(mock), 59 | io: io, 60 | } 61 | 62 | if err := delete(rootOpts, opts); err == nil { 63 | t.Error("create() error = nil, expected error") 64 | return 65 | } 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /internal/cmd/edit/edit.go: -------------------------------------------------------------------------------- 1 | package edit 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/MakeNowJust/heredoc" 8 | "github.com/cli/cli/pkg/iostreams" 9 | "github.com/heaths/gh-label/internal/github" 10 | "github.com/heaths/gh-label/internal/options" 11 | "github.com/heaths/gh-label/internal/utils" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type editOptions struct { 16 | name string 17 | color string 18 | description string 19 | newName string 20 | 21 | // test 22 | client *github.Client 23 | io *iostreams.IOStreams 24 | } 25 | 26 | func EditCmd(globalOpts *options.GlobalOptions) *cobra.Command { 27 | opts := &editOptions{} 28 | cmd := &cobra.Command{ 29 | Use: "edit ", 30 | Short: "Edit the label in the repository", 31 | Example: heredoc.Doc(` 32 | $ gh label edit general --new-name feedback 33 | $ gh label edit feedback --color c046ff --description "User feedback" 34 | `), 35 | Args: cobra.ExactArgs(1), 36 | PreRunE: func(cmd *cobra.Command, args []string) error { 37 | if opts.color != "" { 38 | if color, err := utils.ValidateColor(opts.color); err != nil { 39 | return fmt.Errorf(`invalid flag "color": %s`, err) 40 | } else { 41 | // Set color without "#" prefix. 42 | opts.color = color 43 | } 44 | } 45 | 46 | return nil 47 | }, 48 | RunE: func(cmd *cobra.Command, args []string) error { 49 | opts.name = args[0] 50 | 51 | return edit(globalOpts, opts) 52 | }, 53 | } 54 | 55 | cmd.Flags().StringVarP(&opts.color, "color", "c", "", `The color of the label with or without "#" prefix.`) 56 | cmd.Flags().StringVarP(&opts.description, "description", "d", "", "Description of the label.") 57 | cmd.Flags().StringVarP(&opts.newName, "new-name", "", "", "Rename the label to the given new name.") 58 | 59 | return cmd 60 | } 61 | 62 | func edit(globalOpts *options.GlobalOptions, opts *editOptions) error { 63 | if opts.client == nil { 64 | owner, repo := globalOpts.Repo() 65 | cli := &github.Cli{ 66 | Owner: owner, 67 | Repo: repo, 68 | } 69 | opts.client = github.New(cli) 70 | } 71 | 72 | if opts.io == nil { 73 | opts.io = iostreams.System() 74 | } 75 | 76 | label := github.EditLabel{ 77 | Label: github.Label{ 78 | Name: opts.name, 79 | Color: opts.color, 80 | Description: opts.description, 81 | }, 82 | NewName: opts.newName, 83 | } 84 | 85 | updated, err := opts.client.UpdateLabel(label) 86 | if err != nil { 87 | return fmt.Errorf("failed to create label; error: %w", err) 88 | } 89 | 90 | re := regexp.MustCompile("^https://api.([^/]+)/repos/(.*)$") 91 | matches := re.FindStringSubmatch(updated.URL) 92 | 93 | if opts.io.IsStdoutTTY() { 94 | if label.Name != updated.Name { 95 | fmt.Fprintf(opts.io.Out, "Renamed label '%s' to '%s'\n\n", label.Name, updated.Name) 96 | } else { 97 | fmt.Fprintf(opts.io.Out, "Updated label '%s'\n\n", updated.Name) 98 | } 99 | } 100 | 101 | if len(matches) == 3 { 102 | fmt.Fprintf(opts.io.Out, "https://%s/%s\n", matches[1], matches[2]) 103 | } else { 104 | fmt.Fprintln(opts.io.Out, updated.URL) 105 | } 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /internal/cmd/edit/edit_test.go: -------------------------------------------------------------------------------- 1 | package edit 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/MakeNowJust/heredoc" 8 | "github.com/cli/cli/pkg/iostreams" 9 | "github.com/heaths/gh-label/internal/github" 10 | "github.com/heaths/gh-label/internal/options" 11 | ) 12 | 13 | func Test_edit(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | rename string 17 | tty bool 18 | want string 19 | }{ 20 | { 21 | name: "edit", 22 | want: "https://github.com/heaths/gh-label/labels/test\n", 23 | }, 24 | { 25 | name: "edit (TTY)", 26 | tty: true, 27 | want: heredoc.Doc(`Updated label 'test' 28 | 29 | https://github.com/heaths/gh-label/labels/test 30 | `), 31 | }, 32 | { 33 | name: "renamed (TTY)", 34 | rename: "test2", 35 | tty: true, 36 | want: heredoc.Doc(`Renamed label 'test' to 'test2' 37 | 38 | https://github.com/heaths/gh-label/labels/test2 39 | `), 40 | }, 41 | } 42 | 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | // Set up output streams. 46 | io, _, stdout, _ := iostreams.Test() 47 | io.SetStdoutTTY(tt.tty) 48 | io.SetColorEnabled(true) 49 | 50 | // Set up gh output. 51 | name := "test" 52 | if tt.rename != "" { 53 | name = tt.rename 54 | } 55 | mock := &github.Mock{ 56 | Stdout: *bytes.NewBufferString(heredoc.Docf(`{ 57 | "id": 3315930645, 58 | "node_id": "MDU6TGFiZWwzMzE1OTMwNjQ1", 59 | "url": "https://api.github.com/repos/heaths/gh-label/labels/%[1]s", 60 | "name": "%[1]s", 61 | "color": "112233", 62 | "default": false, 63 | "description": "" 64 | }`, name)), 65 | } 66 | 67 | rootOpts := &options.GlobalOptions{} 68 | opts := &editOptions{ 69 | name: "test", 70 | color: "112233", 71 | newName: tt.rename, 72 | 73 | client: github.New(mock), 74 | io: io, 75 | } 76 | 77 | if err := edit(rootOpts, opts); err != nil { 78 | t.Errorf("edit() error = %v", err) 79 | return 80 | } 81 | 82 | if gotW := stdout.String(); gotW != tt.want { 83 | t.Errorf("edit() = %q, want %q", gotW, tt.want) 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/cmd/export/export.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path" 8 | 9 | "github.com/MakeNowJust/heredoc" 10 | "github.com/cli/cli/pkg/iostreams" 11 | "github.com/heaths/gh-label/internal/github" 12 | "github.com/heaths/gh-label/internal/options" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | type exportOptions struct { 17 | path string 18 | format string 19 | 20 | // test 21 | client *github.Client 22 | io *iostreams.IOStreams 23 | } 24 | 25 | // opts are export options available for testing. 26 | var opts *exportOptions 27 | 28 | func ExportCmd(globalOpts *options.GlobalOptions) *cobra.Command { 29 | opts = &exportOptions{} 30 | cmd := &cobra.Command{ 31 | Use: "export ", 32 | Short: `Export labels from the repository to , or stdout if is "-".`, 33 | Example: heredoc.Doc(` 34 | $ gh label export ./labels.csv 35 | $ gh label export ./labels.json 36 | $ gh label export --format csv - 37 | `), 38 | Args: cobra.ExactArgs(1), 39 | PreRunE: func(cmd *cobra.Command, args []string) error { 40 | if opts.format != "" { 41 | if format, err := github.SupportedOutputFormat(opts.format); err != nil { 42 | return err 43 | } else { 44 | opts.format = format 45 | } 46 | } 47 | 48 | return nil 49 | }, 50 | RunE: func(cmd *cobra.Command, args []string) error { 51 | opts.path = args[0] 52 | if opts.path != "-" { 53 | if opts.format == "" { 54 | opts.format = path.Ext(opts.path) 55 | } 56 | } else if opts.format == "" { 57 | return fmt.Errorf(`--format is required when is "-"`) 58 | } 59 | 60 | if format, err := github.SupportedOutputFormat(opts.format); err != nil { 61 | return fmt.Errorf("%q has unsupported format %q, expected %v", opts.path, opts.format, github.OutputFormats()) 62 | } else { 63 | opts.format = format 64 | } 65 | 66 | return export(globalOpts, opts) 67 | }, 68 | } 69 | 70 | cmd.Flags().StringVarP(&opts.format, "format", "", "", fmt.Sprintf("Format of the file to export. One of %v. The default is the file extension.", github.OutputFormats())) 71 | 72 | return cmd 73 | } 74 | 75 | func export(globalOpts *options.GlobalOptions, opts *exportOptions) error { 76 | if opts.client == nil { 77 | owner, repo := globalOpts.Repo() 78 | cli := &github.Cli{ 79 | Owner: owner, 80 | Repo: repo, 81 | } 82 | opts.client = github.New(cli) 83 | } 84 | 85 | if opts.io == nil { 86 | opts.io = iostreams.System() 87 | } 88 | 89 | labels, err := opts.client.ListLabels("") 90 | if err != nil { 91 | return fmt.Errorf("failed to list labels; error: %w", err) 92 | } 93 | 94 | var w io.Writer 95 | if opts.path == "-" { 96 | w = opts.io.Out 97 | } else { 98 | if file, err := os.Create(opts.path); err != nil { 99 | return fmt.Errorf("failed to create file %q; error: %w", opts.path, err) 100 | } else { 101 | w = file 102 | defer file.Close() 103 | } 104 | } 105 | 106 | if err := labels.Write(github.OutputFormat(opts.format), w); err != nil { 107 | return err 108 | } 109 | 110 | if opts.path != "-" && opts.io.IsStdoutTTY() { 111 | fmt.Fprintf(opts.io.Out, "Exported %d labels to %q\n", len(labels), opts.path) 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /internal/cmd/export/export_test.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/MakeNowJust/heredoc" 10 | "github.com/cli/cli/pkg/iostreams" 11 | "github.com/heaths/gh-label/internal/github" 12 | "github.com/heaths/gh-label/internal/options" 13 | ) 14 | 15 | func Test_ExportCmd(t *testing.T) { 16 | type args struct { 17 | path string 18 | format string 19 | } 20 | 21 | tests := []struct { 22 | name string 23 | args args 24 | want args 25 | wantE bool 26 | }{ 27 | { 28 | name: "csv file", 29 | args: args{ 30 | path: "labels.csv", 31 | }, 32 | want: args{ 33 | path: "labels.csv", 34 | format: "csv", 35 | }, 36 | }, 37 | { 38 | name: "csv stream", 39 | args: args{ 40 | path: "-", 41 | format: "csv", 42 | }, 43 | want: args{ 44 | path: "-", 45 | format: "csv", 46 | }, 47 | }, 48 | { 49 | name: "csv override", 50 | args: args{ 51 | path: "labels.json", 52 | format: "csv", 53 | }, 54 | want: args{ 55 | path: "labels.json", 56 | format: "csv", 57 | }, 58 | }, 59 | { 60 | name: "json file", 61 | args: args{ 62 | path: "labels.json", 63 | }, 64 | want: args{ 65 | path: "labels.json", 66 | format: "json", 67 | }, 68 | }, 69 | { 70 | name: "json stream", 71 | args: args{ 72 | path: "-", 73 | format: "json", 74 | }, 75 | want: args{ 76 | path: "-", 77 | format: "json", 78 | }, 79 | }, 80 | { 81 | name: "json override", 82 | args: args{ 83 | path: "labels.csv", 84 | format: "json", 85 | }, 86 | want: args{ 87 | path: "labels.csv", 88 | format: "json", 89 | }, 90 | }, 91 | { 92 | name: "stream without format", 93 | args: args{ 94 | path: "-", 95 | }, 96 | want: args{ 97 | path: "-", 98 | }, 99 | wantE: true, 100 | }, 101 | { 102 | name: "invalid format", 103 | args: args{ 104 | path: "-", 105 | format: "", 106 | }, 107 | want: args{ 108 | path: "-", 109 | }, 110 | wantE: true, 111 | }, 112 | } 113 | 114 | for _, tt := range tests { 115 | t.Run(tt.name, func(t *testing.T) { 116 | globalOpts := &options.GlobalOptions{} 117 | cmd := ExportCmd(globalOpts) 118 | cmd.SetOut(&bytes.Buffer{}) 119 | cmd.SetErr(&bytes.Buffer{}) 120 | 121 | args := []string{tt.args.path} 122 | if tt.args.format != "" { 123 | args = append(args, "--format", tt.args.format) 124 | } 125 | cmd.SetArgs(args) 126 | 127 | mock := &github.Mock{ 128 | Err: fmt.Errorf("mock"), 129 | } 130 | opts.client = github.New(mock) 131 | 132 | if err := cmd.Execute(); !strings.Contains(err.Error(), "mock") && (err != nil) != tt.wantE { 133 | t.Error("ExportCmd().Execute() expected error") 134 | return 135 | } 136 | 137 | if opts.path != tt.want.path { 138 | t.Errorf("ExportCmd() path = %q, expected %q", opts.path, tt.want.path) 139 | return 140 | } 141 | 142 | if opts.format != tt.want.format { 143 | t.Errorf("ExportCmd() format = %q, expected %q", opts.format, tt.want.format) 144 | return 145 | } 146 | }) 147 | } 148 | } 149 | 150 | func Test_export(t *testing.T) { 151 | type args struct { 152 | format string 153 | stdout string 154 | tty bool 155 | } 156 | 157 | tests := []struct { 158 | name string 159 | args args 160 | wantW string 161 | wantE bool 162 | }{ 163 | { 164 | name: "csv", 165 | args: args{ 166 | format: "csv", 167 | stdout: `{ 168 | "data": { 169 | "repository": { 170 | "labels": { 171 | "nodes": [ 172 | { 173 | "name": "bug", 174 | "color": "d73a4a", 175 | "description": "Something isn't working", 176 | "url": "https://github.com/heaths/gh-label/issues/1" 177 | }, 178 | { 179 | "name": "documentation", 180 | "color": "0075ca", 181 | "description": "Improvements or additions to documentation" 182 | } 183 | ] 184 | } 185 | } 186 | } 187 | }`, 188 | }, 189 | wantW: heredoc.Doc(`name,color,description,url 190 | bug,d73a4a,Something isn't working,https://github.com/heaths/gh-label/issues/1 191 | documentation,0075ca,Improvements or additions to documentation, 192 | `), 193 | }, 194 | { 195 | name: "csv (tty)", 196 | args: args{ 197 | format: "csv", 198 | stdout: `{ 199 | "data": { 200 | "repository": { 201 | "labels": { 202 | "nodes": [ 203 | { 204 | "name": "bug", 205 | "color": "d73a4a", 206 | "description": "Something isn't working", 207 | "url": "https://github.com/heaths/gh-label/issues/1" 208 | }, 209 | { 210 | "name": "documentation", 211 | "color": "0075ca", 212 | "description": "Improvements or additions to documentation" 213 | } 214 | ] 215 | } 216 | } 217 | } 218 | }`, 219 | tty: true, 220 | }, 221 | wantW: heredoc.Doc(`name,color,description,url 222 | bug,d73a4a,Something isn't working,https://github.com/heaths/gh-label/issues/1 223 | documentation,0075ca,Improvements or additions to documentation, 224 | `), 225 | }, 226 | { 227 | name: "json", 228 | args: args{ 229 | format: "json", 230 | stdout: `{ 231 | "data": { 232 | "repository": { 233 | "labels": { 234 | "nodes": [ 235 | { 236 | "name": "bug", 237 | "color": "d73a4a", 238 | "description": "Something isn't working", 239 | "url": "https://github.com/heaths/gh-label/issues/1" 240 | }, 241 | { 242 | "name": "documentation", 243 | "color": "0075ca", 244 | "description": "Improvements or additions to documentation" 245 | } 246 | ] 247 | } 248 | } 249 | } 250 | }`, 251 | }, 252 | wantW: heredoc.Doc(`[ 253 | { 254 | "name": "bug", 255 | "color": "d73a4a", 256 | "description": "Something isn't working", 257 | "url": "https://github.com/heaths/gh-label/issues/1" 258 | }, 259 | { 260 | "name": "documentation", 261 | "color": "0075ca", 262 | "description": "Improvements or additions to documentation" 263 | } 264 | ] 265 | `), 266 | }, 267 | } 268 | 269 | for _, tt := range tests { 270 | t.Run(tt.name, func(t *testing.T) { 271 | // Set up output streams. 272 | io, _, stdout, _ := iostreams.Test() 273 | io.SetStdoutTTY(tt.args.tty) 274 | 275 | // Set up gh output. 276 | mock := &github.Mock{ 277 | Stdout: *bytes.NewBufferString(tt.args.stdout), 278 | } 279 | 280 | rootOpts := &options.GlobalOptions{} 281 | opts := &exportOptions{ 282 | path: "-", 283 | format: tt.args.format, 284 | 285 | client: github.New(mock), 286 | io: io, 287 | } 288 | 289 | if err := export(rootOpts, opts); (err != nil) != tt.wantE { 290 | t.Errorf("export() error = %v, wantE %v", err, tt.wantE) 291 | return 292 | } 293 | 294 | if gotW := stdout.String(); gotW != tt.wantW { 295 | t.Errorf("export() = %q, want %q", gotW, tt.wantW) 296 | } 297 | }) 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /internal/cmd/import/import.go: -------------------------------------------------------------------------------- 1 | package importcmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "path" 10 | 11 | "github.com/MakeNowJust/heredoc" 12 | "github.com/cli/cli/pkg/iostreams" 13 | "github.com/heaths/gh-label/internal/github" 14 | "github.com/heaths/gh-label/internal/options" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | type importOptions struct { 19 | path string 20 | format string 21 | 22 | // test 23 | client *github.Client 24 | fs fs.FS 25 | io *iostreams.IOStreams 26 | } 27 | 28 | // Make available for testing. 29 | var opts *importOptions 30 | 31 | func ImportCmd(globalOpts *options.GlobalOptions) *cobra.Command { 32 | opts = &importOptions{} 33 | cmd := &cobra.Command{ 34 | Use: "import ", 35 | Short: `Import labels into the repository from , or stdin if is "-".`, 36 | Example: heredoc.Doc(` 37 | $ gh label import ./labels.csv 38 | $ gh label import ./labels.json 39 | $ gh label import --format csv - 40 | `), 41 | Args: cobra.ExactArgs(1), 42 | PreRunE: func(cmd *cobra.Command, args []string) error { 43 | if opts.format != "" { 44 | if format, err := github.SupportedOutputFormat(opts.format); err != nil { 45 | return err 46 | } else { 47 | opts.format = format 48 | } 49 | } 50 | 51 | return nil 52 | }, 53 | RunE: func(cmd *cobra.Command, args []string) error { 54 | opts.path = args[0] 55 | if opts.path != "-" { 56 | if opts.format == "" { 57 | opts.format = path.Ext(opts.path) 58 | } 59 | } else if opts.format == "" { 60 | return fmt.Errorf(`--format is required when is "-"`) 61 | } 62 | 63 | if format, err := github.SupportedOutputFormat(opts.format); err != nil { 64 | return fmt.Errorf("%q has unsupported format %q, expected %v", opts.path, opts.format, github.OutputFormats()) 65 | } else { 66 | opts.format = format 67 | } 68 | 69 | return _import(globalOpts, opts) 70 | }, 71 | } 72 | 73 | cmd.Flags().StringVarP(&opts.format, "format", "", "", fmt.Sprintf("Format of the input to parse. One of %v. The default is the file extension.", github.OutputFormats())) 74 | 75 | return cmd 76 | } 77 | 78 | func _import(globalOpts *options.GlobalOptions, opts *importOptions) error { 79 | if opts.client == nil { 80 | owner, repo := globalOpts.Repo() 81 | cli := &github.Cli{ 82 | Owner: owner, 83 | Repo: repo, 84 | } 85 | opts.client = github.New(cli) 86 | } 87 | 88 | if opts.fs == nil { 89 | pwd, err := os.Getwd() 90 | if err != nil { 91 | pwd = "/" 92 | } 93 | opts.fs = os.DirFS(pwd) 94 | } 95 | 96 | if opts.io == nil { 97 | opts.io = iostreams.System() 98 | } 99 | 100 | var r io.Reader 101 | if opts.path == "-" { 102 | r = opts.io.In 103 | } else { 104 | if file, err := opts.fs.Open(opts.path); err != nil { 105 | return fmt.Errorf("failed to open file %q; error: %w", opts.path, err) 106 | } else { 107 | r = file 108 | defer file.Close() 109 | } 110 | } 111 | 112 | labels, err := github.ReadLabels(github.OutputFormat(opts.format), r) 113 | if err != nil { 114 | return fmt.Errorf("failed to read labels; error: %w", err) 115 | } 116 | 117 | if opts.io.IsStdoutTTY() { 118 | fmt.Fprintf(opts.io.Out, "Importing %d label(s) from %q\n\n", len(labels), opts.path) 119 | } 120 | 121 | // TODO: Write progress bar if TTY. 122 | 123 | successes := 0 124 | failures := 0 125 | 126 | for _, label := range labels { 127 | if _, err := opts.client.CreateOrUpdateLabel(label); err != nil { 128 | failures++ 129 | fmt.Fprintf(opts.io.ErrOut, "Failed to import label %q\n", label.Name) 130 | continue 131 | } 132 | 133 | successes++ 134 | } 135 | 136 | if opts.io.IsStdoutTTY() { 137 | if failures > 0 { 138 | fmt.Fprintf(opts.io.ErrOut, "\n") 139 | } 140 | 141 | fmt.Fprintf(opts.io.Out, "Successfully imported %d, failed to import %d label(s)\n", successes, failures) 142 | } 143 | 144 | if successes == 0 { 145 | return errors.New("failed to import all labels") 146 | } 147 | 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /internal/cmd/import/import_test.go: -------------------------------------------------------------------------------- 1 | package importcmd 2 | 3 | // cSpell:ignore fstest 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "testing" 9 | "testing/fstest" 10 | 11 | "github.com/MakeNowJust/heredoc" 12 | "github.com/cli/cli/pkg/iostreams" 13 | "github.com/heaths/gh-label/internal/github" 14 | "github.com/heaths/gh-label/internal/options" 15 | ) 16 | 17 | var ( 18 | csvData = []byte(heredoc.Doc(`name,color,description,url 19 | bug,d73a4a,Something isn't working,https://github.com/heaths/gh-label/issues/1 20 | `)) 21 | 22 | jsonData = []byte(heredoc.Doc(`[ 23 | { 24 | "name": "bug", 25 | "color": "d73a4a", 26 | "description": "Something isn't working", 27 | "url": "https://github.com/heaths/gh-label/issues/1" 28 | } 29 | ]`)) 30 | 31 | jsonLabel = bytes.Trim(jsonData, "[]") 32 | ) 33 | 34 | func Test_ImportCmd(t *testing.T) { 35 | type args struct { 36 | path string 37 | format string 38 | data []byte 39 | } 40 | 41 | tests := []struct { 42 | name string 43 | args args 44 | want args 45 | wantE bool 46 | }{ 47 | { 48 | name: "csv file", 49 | args: args{ 50 | path: "labels.csv", 51 | data: csvData, 52 | }, 53 | want: args{ 54 | path: "labels.csv", 55 | format: "csv", 56 | }, 57 | }, 58 | { 59 | name: "csv stream", 60 | args: args{ 61 | path: "-", 62 | format: "csv", 63 | data: csvData, 64 | }, 65 | want: args{ 66 | path: "-", 67 | format: "csv", 68 | }, 69 | }, 70 | { 71 | name: "csv override", 72 | args: args{ 73 | path: "labels.json", 74 | format: "csv", 75 | data: csvData, 76 | }, 77 | want: args{ 78 | path: "labels.json", 79 | format: "csv", 80 | }, 81 | }, 82 | { 83 | name: "json file", 84 | args: args{ 85 | path: "labels.json", 86 | data: jsonData, 87 | }, 88 | want: args{ 89 | path: "labels.json", 90 | format: "json", 91 | }, 92 | }, 93 | { 94 | name: "json stream", 95 | args: args{ 96 | path: "-", 97 | format: "json", 98 | data: jsonData, 99 | }, 100 | want: args{ 101 | path: "-", 102 | format: "json", 103 | }, 104 | }, 105 | { 106 | name: "json override", 107 | args: args{ 108 | path: "labels.csv", 109 | format: "json", 110 | data: jsonData, 111 | }, 112 | want: args{ 113 | path: "labels.csv", 114 | format: "json", 115 | }, 116 | }, 117 | { 118 | name: "stream without format", 119 | args: args{ 120 | path: "-", 121 | }, 122 | want: args{ 123 | path: "-", 124 | }, 125 | wantE: true, 126 | }, 127 | { 128 | name: "invalid format", 129 | args: args{ 130 | path: "-", 131 | format: "", 132 | }, 133 | want: args{ 134 | path: "-", 135 | }, 136 | wantE: true, 137 | }, 138 | } 139 | 140 | for _, tt := range tests { 141 | t.Run(tt.name, func(t *testing.T) { 142 | globalOpts := &options.GlobalOptions{} 143 | cmd := ImportCmd(globalOpts) 144 | cmd.SetOut(&bytes.Buffer{}) 145 | cmd.SetErr(&bytes.Buffer{}) 146 | 147 | args := []string{tt.args.path} 148 | if tt.args.format != "" { 149 | args = append(args, "--format", tt.args.format) 150 | } 151 | cmd.SetArgs(args) 152 | 153 | mock := &github.Mock{ 154 | Stdout: *bytes.NewBuffer(jsonLabel), 155 | } 156 | opts.client = github.New(mock) 157 | 158 | fs := fstest.MapFS{} 159 | fs[tt.args.path] = &fstest.MapFile{ 160 | Data: tt.args.data, 161 | } 162 | opts.fs = fs 163 | 164 | io, stdin, _, _ := iostreams.Test() 165 | opts.io = io 166 | if tt.args.path == "-" { 167 | stdin.Write(tt.args.data) 168 | } 169 | 170 | if err := cmd.Execute(); (err != nil) != tt.wantE { 171 | t.Errorf("ImportCmd().Execute() error = %v, expected %v", err, tt.wantE) 172 | return 173 | } 174 | 175 | if opts.path != tt.want.path { 176 | t.Errorf("ImportCmd() path = %q, expected %q", opts.path, tt.want.path) 177 | return 178 | } 179 | 180 | if opts.format != tt.want.format { 181 | t.Errorf("ImportCmd() format = %q, expected %q", opts.format, tt.want.format) 182 | return 183 | } 184 | }) 185 | } 186 | } 187 | 188 | func Test_import(t *testing.T) { 189 | type args struct { 190 | format string 191 | stdin []byte 192 | tty bool 193 | err error 194 | } 195 | 196 | tests := []struct { 197 | name string 198 | args args 199 | wantW string 200 | wantE bool 201 | }{ 202 | { 203 | name: "csv", 204 | args: args{ 205 | format: "csv", 206 | stdin: csvData, 207 | }, 208 | }, 209 | { 210 | name: "csv (tty)", 211 | args: args{ 212 | format: "csv", 213 | stdin: csvData, 214 | tty: true, 215 | }, 216 | wantW: heredoc.Doc(`Importing 1 label(s) from "-" 217 | 218 | Successfully imported 1, failed to import 0 label(s) 219 | `), 220 | }, 221 | { 222 | name: "json", 223 | args: args{ 224 | format: "json", 225 | stdin: jsonData, 226 | }, 227 | }, 228 | { 229 | name: "json (tty)", 230 | args: args{ 231 | format: "json", 232 | stdin: jsonData, 233 | tty: true, 234 | }, 235 | wantW: heredoc.Doc(`Importing 1 label(s) from "-" 236 | 237 | Successfully imported 1, failed to import 0 label(s) 238 | `), 239 | }, 240 | { 241 | name: "all failed", 242 | args: args{ 243 | format: "json", 244 | stdin: jsonData, 245 | err: errors.New("failed"), 246 | }, 247 | wantE: true, 248 | }, 249 | } 250 | 251 | for _, tt := range tests { 252 | t.Run(tt.name, func(t *testing.T) { 253 | // Set up streams. 254 | io, stdin, stdout, _ := iostreams.Test() 255 | io.SetStdoutTTY(tt.args.tty) 256 | stdin.Write(tt.args.stdin) 257 | 258 | // Set up gh output. 259 | mock := &github.Mock{ 260 | Stdout: *bytes.NewBuffer(jsonLabel), 261 | Err: tt.args.err, 262 | } 263 | 264 | rootOpts := &options.GlobalOptions{} 265 | opts := &importOptions{ 266 | path: "-", 267 | format: tt.args.format, 268 | 269 | client: github.New(mock), 270 | io: io, 271 | } 272 | 273 | if err := _import(rootOpts, opts); (err != nil) != tt.wantE { 274 | t.Errorf("_import() error = %v, wantE %v", err, tt.wantE) 275 | return 276 | } 277 | 278 | if gotW := stdout.String(); gotW != tt.wantW { 279 | t.Errorf("_import() = %q, want %q", gotW, tt.wantW) 280 | } 281 | }) 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /internal/cmd/list/list.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/MakeNowJust/heredoc" 7 | "github.com/cli/cli/pkg/iostreams" 8 | "github.com/cli/cli/utils" 9 | "github.com/heaths/gh-label/internal/github" 10 | "github.com/heaths/gh-label/internal/options" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | type listOptions struct { 15 | label string 16 | 17 | // test 18 | client *github.Client 19 | io *iostreams.IOStreams 20 | } 21 | 22 | func ListCmd(globalOpts *options.GlobalOptions) *cobra.Command { 23 | opts := &listOptions{} 24 | cmd := &cobra.Command{ 25 | Use: "list [name]", 26 | Short: "List labels in the repository, optionally matching substring [name] in the label name or description", 27 | Example: heredoc.Doc(` 28 | $ gh label list 29 | $ gh label list service 30 | `), 31 | Args: cobra.MaximumNArgs(1), 32 | RunE: func(cmd *cobra.Command, args []string) error { 33 | if len(args) > 0 { 34 | opts.label = args[0] 35 | } 36 | return list(globalOpts, opts) 37 | }, 38 | } 39 | 40 | return cmd 41 | } 42 | 43 | func list(globalOpts *options.GlobalOptions, opts *listOptions) error { 44 | if opts.client == nil { 45 | owner, repo := globalOpts.Repo() 46 | cli := &github.Cli{ 47 | Owner: owner, 48 | Repo: repo, 49 | } 50 | opts.client = github.New(cli) 51 | } 52 | 53 | if opts.io == nil { 54 | opts.io = iostreams.System() 55 | } 56 | 57 | labels, err := opts.client.ListLabels(opts.label) 58 | if err != nil { 59 | return fmt.Errorf("failed to list labels; error: %w", err) 60 | } 61 | 62 | io := opts.io 63 | cs := io.ColorScheme() 64 | 65 | colorizer := func(color string) func(string) string { 66 | return func(s string) string { 67 | return cs.HexToRGB(color, s) 68 | } 69 | } 70 | 71 | if io.IsStdoutTTY() { 72 | fmt.Fprintf(io.Out, "Showing %d labels\n\n", len(labels)) 73 | } 74 | 75 | printer := utils.NewTablePrinter(io) 76 | for _, label := range labels { 77 | color := label.Color 78 | printer.AddField(label.Name, nil, colorizer(color)) 79 | if printer.IsTTY() { 80 | color = "#" + color 81 | } 82 | printer.AddField(color, nil, nil) 83 | printer.AddField(label.Description, nil, cs.ColorFromString("gray")) 84 | printer.EndRow() 85 | } 86 | _ = printer.Render() 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/cmd/list/list_test.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | // cSpell:ignoreRegExp /[0-9A-Fa-f]{6}/ 4 | 5 | import ( 6 | "bytes" 7 | "testing" 8 | 9 | "github.com/MakeNowJust/heredoc" 10 | "github.com/cli/cli/pkg/iostreams" 11 | "github.com/heaths/gh-label/internal/github" 12 | "github.com/heaths/gh-label/internal/options" 13 | ) 14 | 15 | func Test_list(t *testing.T) { 16 | type args struct { 17 | stdout string 18 | tty bool 19 | } 20 | 21 | tests := []struct { 22 | name string 23 | args args 24 | wantW string 25 | wantE bool 26 | }{ 27 | { 28 | name: "single page", 29 | args: args{ 30 | stdout: `{ 31 | "data": { 32 | "repository": { 33 | "labels": { 34 | "nodes": [ 35 | { 36 | "name": "bug", 37 | "color": "d73a4a", 38 | "description": "Something isn't working" 39 | }, 40 | { 41 | "name": "documentation", 42 | "color": "0075ca", 43 | "description": "Improvements or additions to documentation" 44 | } 45 | ] 46 | } 47 | } 48 | } 49 | }`, 50 | }, 51 | wantW: heredoc.Docf(`bug%[1]sd73a4a%[1]sSomething isn't working 52 | documentation%[1]s0075ca%[1]sImprovements or additions to documentation 53 | `, "\t"), 54 | }, 55 | { 56 | name: "single page (TTY)", 57 | args: args{ 58 | stdout: `{ 59 | "data": { 60 | "repository": { 61 | "labels": { 62 | "nodes": [ 63 | { 64 | "name": "bug", 65 | "color": "d73a4a", 66 | "description": "Something isn't working" 67 | }, 68 | { 69 | "name": "documentation", 70 | "color": "0075ca", 71 | "description": "Improvements or additions to documentation" 72 | } 73 | ] 74 | } 75 | } 76 | } 77 | }`, 78 | tty: true, 79 | }, 80 | wantW: heredoc.Docf(`Showing 2 labels 81 | 82 | bug #d73a4a %[1]s[0;90mSomething isn't working%[1]s[0m 83 | documentation #0075ca %[1]s[0;90mImprovements or additions to documentation%[1]s[0m 84 | `, "\x1b"), 85 | }, 86 | { 87 | name: "multiple pages", 88 | args: args{ 89 | // Tests workaround for invalid JSON until https://github.com/cli/cli/issues/1268 is resolved. 90 | stdout: `{ 91 | "data": { 92 | "repository": { 93 | "labels": { 94 | "nodes": [ 95 | { 96 | "name": "bug", 97 | "color": "d73a4a", 98 | "description": "Something isn't working" 99 | }, 100 | { 101 | "name": "documentation", 102 | "color": "0075ca", 103 | "description": "Improvements or additions to documentation" 104 | } 105 | ], 106 | "pageInfo":{"hasNextPage":true,"endCursor":"abcd1234"}}}}} 107 | { 108 | "data": { 109 | "repository": { 110 | "labels": { 111 | "nodes": [ 112 | { 113 | "name": "duplicate", 114 | "color": "cfd3d7", 115 | "description": "This issue or pull request already exists" 116 | }, 117 | { 118 | "name": "enhancement", 119 | "color": "a2eeef", 120 | "description": "New feature or request" 121 | } 122 | ], 123 | "pageInfo":{"hasNextPage":false}}}}}`, 124 | }, 125 | wantW: heredoc.Docf(`bug%[1]sd73a4a%[1]sSomething isn't working 126 | documentation%[1]s0075ca%[1]sImprovements or additions to documentation 127 | duplicate%[1]scfd3d7%[1]sThis issue or pull request already exists 128 | enhancement%[1]sa2eeef%[1]sNew feature or request 129 | `, "\t"), 130 | }, 131 | } 132 | 133 | for _, tt := range tests { 134 | t.Run(tt.name, func(t *testing.T) { 135 | // Set up output streams. 136 | io, _, stdout, _ := iostreams.Test() 137 | io.SetStdoutTTY(tt.args.tty) 138 | io.SetColorEnabled(true) 139 | 140 | // Set up gh output. 141 | mock := &github.Mock{ 142 | Stdout: *bytes.NewBufferString(tt.args.stdout), 143 | } 144 | 145 | rootOpts := &options.GlobalOptions{} 146 | opts := &listOptions{ 147 | client: github.New(mock), 148 | io: io, 149 | } 150 | 151 | if err := list(rootOpts, opts); (err != nil) != tt.wantE { 152 | t.Errorf("list() error = %v, wantE %v", err, tt.wantE) 153 | return 154 | } 155 | 156 | if gotW := stdout.String(); gotW != tt.wantW { 157 | t.Errorf("list() = %q, want %q", gotW, tt.wantW) 158 | } 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/github/cli.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | 8 | "github.com/cli/safeexec" 9 | ) 10 | 11 | type Cli struct { 12 | Owner string 13 | Repo string 14 | } 15 | 16 | func (cli *Cli) CreateLabel(label Label) (bytes.Buffer, error) { 17 | args := []string{ 18 | "/repos/:owner/:repo/labels", 19 | "-X", "POST", 20 | "-F", fmt.Sprintf("owner=%s", cli.Owner), 21 | "-F", fmt.Sprintf("repo=%s", cli.Repo), 22 | "-F", fmt.Sprintf("name=%s", label.Name), 23 | "-f", fmt.Sprintf("color=%s", label.Color), 24 | } 25 | 26 | if label.Description != "" { 27 | args = append(args, "-F", fmt.Sprintf("description=%s", label.Description)) 28 | } 29 | 30 | stdout, _, err := run(args...) 31 | if err != nil { 32 | return bytes.Buffer{}, err 33 | } 34 | 35 | return stdout, nil 36 | } 37 | 38 | func (cli *Cli) ListLabels(substr string) (bytes.Buffer, error) { 39 | query := `query ($owner: String!, $repo: String!, $label: String, $endCursor: String) { 40 | repository(name: $repo, owner: $owner) { 41 | labels(query: $label, orderBy: {field: NAME, direction: ASC}, first: 100, after: $endCursor) { 42 | nodes { 43 | name 44 | color 45 | description 46 | } 47 | pageInfo { 48 | hasNextPage 49 | endCursor 50 | } 51 | } 52 | } 53 | }` 54 | 55 | args := []string{ 56 | "graphql", 57 | "--paginate", 58 | "-F", fmt.Sprintf("owner=%s", cli.Owner), 59 | "-F", fmt.Sprintf("repo=%s", cli.Repo), 60 | "-F", fmt.Sprintf("label=%s", substr), 61 | "-f", fmt.Sprintf("query=%s", query), 62 | } 63 | 64 | stdout, _, err := run(args...) 65 | if err != nil { 66 | return bytes.Buffer{}, err 67 | } 68 | 69 | return stdout, nil 70 | } 71 | 72 | func (cli *Cli) DeleteLabel(name string) error { 73 | args := []string{ 74 | fmt.Sprintf("/repos/:owner/:repo/labels/%s", name), 75 | "-X", "DELETE", 76 | "-F", fmt.Sprintf("owner=%s", cli.Owner), 77 | "-F", fmt.Sprintf("repo=%s", cli.Repo), 78 | } 79 | 80 | _, _, err := run(args...) 81 | return err 82 | } 83 | 84 | func (cli *Cli) UpdateLabel(label EditLabel) (bytes.Buffer, error) { 85 | args := []string{ 86 | fmt.Sprintf("/repos/:owner/:repo/labels/%s", label.Name), 87 | "-X", "PATCH", 88 | "-F", fmt.Sprintf("owner=%s", cli.Owner), 89 | "-F", fmt.Sprintf("repo=%s", cli.Repo), 90 | } 91 | 92 | if label.Color != "" { 93 | args = append(args, "-f", fmt.Sprintf("color=%s", label.Color)) 94 | } 95 | 96 | if label.Description != "" { 97 | args = append(args, "-F", fmt.Sprintf("description=%s", label.Description)) 98 | } 99 | 100 | if label.NewName != "" { 101 | args = append(args, "-F", fmt.Sprintf("new_name=%s", label.NewName)) 102 | } 103 | 104 | stdout, _, err := run(args...) 105 | if err != nil { 106 | return bytes.Buffer{}, err 107 | } 108 | 109 | return stdout, nil 110 | } 111 | 112 | func run(args ...string) (stdout, stderr bytes.Buffer, err error) { 113 | bin, err := safeexec.LookPath("gh") 114 | if err != nil { 115 | err = fmt.Errorf("cannot find gh; is it installed? error: %w", err) 116 | return 117 | } 118 | 119 | // Always prepend arguments passed to every command. 120 | args = append([]string{"api", "-H", "accept:application/vnd.github.v3+json"}, args...) 121 | 122 | cmd := exec.Command(bin, args...) 123 | cmd.Stdout = &stdout 124 | cmd.Stderr = &stderr 125 | 126 | err = cmd.Run() 127 | if err != nil { 128 | err = fmt.Errorf("gh returned error: %w, stderr: %s", err, stderr.String()) 129 | return 130 | } 131 | 132 | return 133 | } 134 | -------------------------------------------------------------------------------- /internal/github/client.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | type EditLabel struct { 11 | Label 12 | NewName string `json:"new_name,omitempty"` 13 | } 14 | 15 | type Client struct { 16 | labels LabelsService 17 | } 18 | 19 | type LabelsService interface { 20 | CreateLabel(label Label) (bytes.Buffer, error) 21 | DeleteLabel(name string) error 22 | ListLabels(substr string) (bytes.Buffer, error) 23 | UpdateLabel(label EditLabel) (bytes.Buffer, error) 24 | } 25 | 26 | func New(labels LabelsService) *Client { 27 | if labels == nil { 28 | labels = &Cli{ 29 | Owner: ":owner", 30 | Repo: ":repo", 31 | } 32 | } 33 | 34 | return &Client{ 35 | labels, 36 | } 37 | } 38 | 39 | func (c *Client) CreateLabel(label Label) (Label, error) { 40 | buf, err := c.labels.CreateLabel(label) 41 | if err != nil { 42 | return Label{}, err 43 | } 44 | 45 | label = Label{} 46 | if err = json.Unmarshal(buf.Bytes(), &label); err != nil { 47 | return Label{}, fmt.Errorf("failed to read label; error: %w, data: %s", err, buf.String()) 48 | } 49 | 50 | return label, nil 51 | } 52 | 53 | func (c *Client) CreateOrUpdateLabel(label Label) (Label, error) { 54 | l, err := c.CreateLabel(label) 55 | if err != nil { 56 | if strings.Contains(err.Error(), "422") { 57 | return c.UpdateLabel(EditLabel{label, ""}) 58 | } 59 | return Label{}, err 60 | } 61 | 62 | return l, nil 63 | } 64 | 65 | func (c *Client) DeleteLabel(name string) error { 66 | return c.labels.DeleteLabel(name) 67 | } 68 | 69 | func (c *Client) ListLabels(substr string) (Labels, error) { 70 | buf, err := c.labels.ListLabels(substr) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | type response struct { 76 | Data struct { 77 | Repository struct { 78 | Labels struct { 79 | Nodes Labels 80 | } 81 | } 82 | } 83 | } 84 | 85 | var labels Labels 86 | 87 | // Work around https://github.com/cli/cli/issues/1268 by splitting responses after cursor info. 88 | for _, data := range bytes.SplitAfter(buf.Bytes(), []byte("}}}}}")) { 89 | if len(data) == 0 { 90 | break 91 | } 92 | 93 | var resp response 94 | if err = json.Unmarshal(data, &resp); err != nil { 95 | return nil, fmt.Errorf("failed to read labels; error: %w, data: %s", err, data) 96 | } 97 | 98 | labels = append(labels, resp.Data.Repository.Labels.Nodes...) 99 | } 100 | 101 | return labels, nil 102 | } 103 | 104 | func (c *Client) UpdateLabel(label EditLabel) (Label, error) { 105 | buf, err := c.labels.UpdateLabel(label) 106 | if err != nil { 107 | return Label{}, err 108 | } 109 | 110 | updated := Label{} 111 | if err = json.Unmarshal(buf.Bytes(), &updated); err != nil { 112 | return Label{}, fmt.Errorf("failed to read label; error: %w, data: %s", err, buf.String()) 113 | } 114 | 115 | return updated, nil 116 | } 117 | 118 | type Mock struct { 119 | Stdout bytes.Buffer 120 | Err error 121 | } 122 | 123 | func (m *Mock) CreateLabel(label Label) (bytes.Buffer, error) { 124 | return m.Stdout, m.Err 125 | } 126 | 127 | func (m *Mock) ListLabels(substr string) (bytes.Buffer, error) { 128 | return m.Stdout, m.Err 129 | } 130 | 131 | func (m *Mock) DeleteLabel(name string) error { 132 | return m.Err 133 | } 134 | 135 | func (m *Mock) UpdateLabel(label EditLabel) (bytes.Buffer, error) { 136 | return m.Stdout, m.Err 137 | } 138 | -------------------------------------------------------------------------------- /internal/github/client_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/MakeNowJust/heredoc" 10 | ) 11 | 12 | func Test_New(t *testing.T) { 13 | client := New(nil) 14 | if cli, ok := client.labels.(*Cli); !ok { 15 | t.Errorf("Client.labels is not expected type *Cli") 16 | } else if cli.Owner != ":owner" || cli.Repo != ":repo" { 17 | t.Errorf(`Client.labels is %v, want &{:owner :repo}`, cli) 18 | } 19 | } 20 | 21 | func Test_New_Mock(t *testing.T) { 22 | mock := &Mock{} 23 | client := New(mock) 24 | if mock != client.labels { 25 | t.Errorf("Client.labels is not expected type Mock") 26 | } 27 | } 28 | 29 | func Test_CreateLabel(t *testing.T) { 30 | tests := []struct { 31 | name string 32 | stdout bytes.Buffer 33 | err error 34 | want Label 35 | wantE bool 36 | }{ 37 | { 38 | name: "gh error", 39 | err: errors.New("gh exited with code 1"), 40 | wantE: true, 41 | }, 42 | { 43 | name: "deserialization error", 44 | stdout: *bytes.NewBufferString("invalid JSON"), 45 | wantE: true, 46 | }, 47 | { 48 | name: "success", 49 | stdout: *bytes.NewBufferString(heredoc.Doc(`{ 50 | "name": "test", 51 | "color": "112233", 52 | "description": "testing" 53 | }`)), 54 | want: Label{ 55 | Name: "test", 56 | Color: "112233", 57 | Description: "testing", 58 | }, 59 | }, 60 | } 61 | for _, tt := range tests { 62 | t.Run(tt.name, func(t *testing.T) { 63 | mock := Mock{ 64 | Stdout: tt.stdout, 65 | Err: tt.err, 66 | } 67 | client := Client{ 68 | labels: &mock, 69 | } 70 | label := Label{ 71 | Name: "test", 72 | Color: "112233", 73 | Description: "testing", 74 | } 75 | if got, err := client.CreateLabel(label); (err != nil) != tt.wantE { 76 | t.Errorf("CreateLabel() error = %v, want: %v", err, tt.wantE) 77 | } else if got != tt.want { 78 | t.Errorf("CreateLabel() = %v, want: %v", got, tt.want) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func Test_DeleteLabel(t *testing.T) { 85 | tests := []struct { 86 | name string 87 | err error 88 | wantE bool 89 | }{ 90 | { 91 | name: "gh error", 92 | err: errors.New("gh exited with code 1"), 93 | wantE: true, 94 | }, 95 | { 96 | name: "success", 97 | }, 98 | } 99 | for _, tt := range tests { 100 | t.Run(tt.name, func(t *testing.T) { 101 | mock := Mock{ 102 | Err: tt.err, 103 | } 104 | client := Client{ 105 | labels: &mock, 106 | } 107 | if err := client.DeleteLabel("test"); (err != nil) != tt.wantE { 108 | t.Errorf("DeleteLabel() error = %v, want: %v", err, tt.wantE) 109 | } 110 | }) 111 | } 112 | } 113 | 114 | func Test_ListLabels(t *testing.T) { 115 | tests := []struct { 116 | name string 117 | stdout bytes.Buffer 118 | err error 119 | want Labels 120 | wantE bool 121 | }{ 122 | { 123 | name: "gh error", 124 | err: errors.New("gh exited with code 1"), 125 | wantE: true, 126 | }, 127 | { 128 | name: "deserialization error", 129 | stdout: *bytes.NewBufferString("invalid JSON"), 130 | wantE: true, 131 | }, 132 | { 133 | name: "success with single page", 134 | stdout: *bytes.NewBufferString(heredoc.Doc(`{ 135 | "data": { 136 | "repository": { 137 | "labels": { 138 | "nodes": [ 139 | { 140 | "name": "test", 141 | "color": "112233", 142 | "description": "testing" 143 | } 144 | ], 145 | "pageInfo":{"hasNextPage":true,"endCursor":"abcd1234"}}}}}`)), 146 | want: Labels{ 147 | { 148 | Name: "test", 149 | Color: "112233", 150 | Description: "testing", 151 | }, 152 | }, 153 | }, 154 | // cSpell:ignore efgh5678 155 | { 156 | name: "success with multiple pages", 157 | stdout: *bytes.NewBufferString(heredoc.Doc(`{ 158 | "data": { 159 | "repository": { 160 | "labels": { 161 | "nodes": [ 162 | { 163 | "name": "test", 164 | "color": "112233", 165 | "description": "testing" 166 | } 167 | ], 168 | "pageInfo":{"hasNextPage":true,"endCursor":"abcd1234"}}}}}{ 169 | "data": { 170 | "repository": { 171 | "labels": { 172 | "nodes": [ 173 | { 174 | "name": "test2", 175 | "color": "223344", 176 | "description": "testing again" 177 | } 178 | ], 179 | "pageInfo":{"hasNextPage":false,"endCursor":"efgh5678"}}}}}`)), 180 | want: Labels{ 181 | { 182 | Name: "test", 183 | Color: "112233", 184 | Description: "testing", 185 | }, 186 | { 187 | Name: "test2", 188 | Color: "223344", 189 | Description: "testing again", 190 | }, 191 | }, 192 | }, 193 | } 194 | for _, tt := range tests { 195 | t.Run(tt.name, func(t *testing.T) { 196 | mock := Mock{ 197 | Stdout: tt.stdout, 198 | Err: tt.err, 199 | } 200 | client := Client{ 201 | labels: &mock, 202 | } 203 | if got, err := client.ListLabels(""); (err != nil) != tt.wantE { 204 | t.Errorf("ListLabels() error = %v, want: %v", err, tt.wantE) 205 | } else if !reflect.DeepEqual(got, tt.want) { 206 | t.Errorf("ListLabels() = %v, want: %v", got, tt.want) 207 | } 208 | }) 209 | } 210 | } 211 | 212 | func Test_UpdateLabel(t *testing.T) { 213 | tests := []struct { 214 | name string 215 | stdout bytes.Buffer 216 | err error 217 | want Label 218 | wantE bool 219 | }{ 220 | { 221 | name: "gh error", 222 | err: errors.New("gh exited with code 1"), 223 | wantE: true, 224 | }, 225 | { 226 | name: "deserialization error", 227 | stdout: *bytes.NewBufferString("invalid JSON"), 228 | wantE: true, 229 | }, 230 | { 231 | name: "success", 232 | stdout: *bytes.NewBufferString(heredoc.Doc(`{ 233 | "name": "renamed", 234 | "color": "112233", 235 | "description": "testing" 236 | }`)), 237 | want: Label{ 238 | Name: "renamed", 239 | Color: "112233", 240 | Description: "testing", 241 | }, 242 | }, 243 | } 244 | for _, tt := range tests { 245 | t.Run(tt.name, func(t *testing.T) { 246 | mock := Mock{ 247 | Stdout: tt.stdout, 248 | Err: tt.err, 249 | } 250 | client := Client{ 251 | labels: &mock, 252 | } 253 | label := EditLabel{ 254 | Label: Label{ 255 | Name: "test", 256 | Color: "112233", 257 | Description: "testing", 258 | }, 259 | NewName: "renamed", 260 | } 261 | if got, err := client.UpdateLabel(label); (err != nil) != tt.wantE { 262 | t.Errorf("UpdateLabel() error = %v, want: %v", err, tt.wantE) 263 | } else if got != tt.want { 264 | t.Errorf("UpdateLabel() = %v, want: %v", got, tt.want) 265 | } 266 | }) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /internal/github/labels.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/csv" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | "github.com/heaths/gh-label/internal/utils" 11 | ) 12 | 13 | const labelFields = 4 14 | 15 | type Label struct { 16 | Name string `json:"name"` 17 | Color string `json:"color"` 18 | Description string `json:"description,omitempty"` 19 | URL string `json:"url,omitempty"` 20 | } 21 | 22 | type Labels []Label 23 | 24 | type OutputFormat string 25 | 26 | const ( 27 | CSV OutputFormat = "csv" 28 | JSON OutputFormat = "json" 29 | ) 30 | 31 | func SupportedOutputFormat(format string) (string, error) { 32 | format = strings.TrimPrefix(format, ".") 33 | format = strings.ToLower(format) 34 | formats := OutputFormats() 35 | 36 | for _, str := range formats { 37 | if str == format { 38 | return format, nil 39 | } 40 | } 41 | 42 | return "", fmt.Errorf("unsupported format %q, expected %v", format, formats) 43 | } 44 | 45 | func OutputFormats() []string { 46 | // These must remain sorted. 47 | return []string{"csv", "json"} 48 | } 49 | 50 | func (label *Label) strings() []string { 51 | return []string{ 52 | label.Name, 53 | label.Color, 54 | label.Description, 55 | label.URL, 56 | } 57 | } 58 | 59 | func (labels *Labels) headers() []string { 60 | return []string{ 61 | "name", 62 | "color", 63 | "description", 64 | "url", 65 | } 66 | } 67 | 68 | func (labels *Labels) strings() [][]string { 69 | arr := make([][]string, len(*labels)) 70 | for i, elem := range *labels { 71 | arr[i] = elem.strings() 72 | } 73 | return arr 74 | } 75 | 76 | func (labels *Labels) Write(format OutputFormat, w io.Writer) error { 77 | if format == CSV { 78 | csv := csv.NewWriter(w) 79 | if err := csv.Write(labels.headers()); err != nil { 80 | return err 81 | } 82 | return csv.WriteAll(labels.strings()) 83 | } 84 | if format == JSON { 85 | json := json.NewEncoder(w) 86 | json.SetIndent("", " ") 87 | return json.Encode(*labels) 88 | } 89 | return fmt.Errorf("unknown format %v", format) 90 | } 91 | 92 | func ReadLabels(format OutputFormat, r io.Reader) (Labels, error) { 93 | // Start with capacity for 10 labels. A new repo currently starts with 9. 94 | labels := make(Labels, 0, 10) 95 | 96 | if format == CSV { 97 | csv := csv.NewReader(r) 98 | csv.FieldsPerRecord = labelFields 99 | csv.ReuseRecord = true 100 | csv.TrimLeadingSpace = true 101 | 102 | for { 103 | record, err := csv.Read() 104 | if err == io.EOF { 105 | break 106 | } 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | if utils.AreEqualStrings(record, labels.headers()) { 112 | continue 113 | } 114 | 115 | label, err := readLabel(record) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | labels = append(labels, *label) 121 | } 122 | 123 | return labels, nil 124 | } 125 | 126 | if format == JSON { 127 | json := json.NewDecoder(r) 128 | if err := json.Decode(&labels); err != nil { 129 | return nil, err 130 | } 131 | 132 | return labels, nil 133 | } 134 | 135 | return nil, fmt.Errorf("unknown format %v", format) 136 | } 137 | 138 | func readLabel(record []string) (*Label, error) { 139 | if len(record) != labelFields { 140 | return nil, fmt.Errorf("expected %d label fields, got %d", labelFields, len(record)) 141 | } 142 | 143 | label := &Label{ 144 | record[0], 145 | record[1], 146 | record[2], 147 | record[3], 148 | } 149 | 150 | return label, nil 151 | } 152 | -------------------------------------------------------------------------------- /internal/github/labels_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/MakeNowJust/heredoc" 9 | ) 10 | 11 | func TestSupportedOutputFormats(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | want string 15 | wantE bool 16 | }{ 17 | { 18 | name: "csv", 19 | want: "csv", 20 | }, 21 | { 22 | name: "CSV", 23 | want: "csv", 24 | }, 25 | { 26 | name: ".csv", 27 | want: "csv", 28 | }, 29 | { 30 | name: "json", 31 | want: "json", 32 | }, 33 | { 34 | name: "JSON", 35 | want: "json", 36 | }, 37 | { 38 | name: ".json", 39 | want: "json", 40 | }, 41 | { 42 | name: "unknown", 43 | wantE: true, 44 | }, 45 | { 46 | name: "", 47 | wantE: true, 48 | }, 49 | { 50 | name: "a", 51 | wantE: true, 52 | }, 53 | } 54 | 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(t *testing.T) { 57 | if got, err := SupportedOutputFormat(tt.name); (err != nil) != tt.wantE { 58 | t.Errorf("SupportedOutputFormat() error = %v, expected error %v", err, tt.wantE) 59 | } else if got != tt.want { 60 | t.Errorf("SupportedOutputFormat() = %v, expected %v", got, tt.want) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | func TestOutputFormats(t *testing.T) { 67 | got := OutputFormats() 68 | want := []string{"csv", "json"} 69 | 70 | if !reflect.DeepEqual(got, want) { 71 | t.Errorf("OutputFormats() = %v, expected %v", got, want) 72 | } 73 | } 74 | 75 | func TestLabel_strings(t *testing.T) { 76 | label := Label{ 77 | Name: "test", 78 | Color: "FF0000", 79 | Description: "a test", 80 | URL: "https://github.com", 81 | } 82 | 83 | got := label.strings() 84 | want := []string{ 85 | "test", 86 | "FF0000", 87 | "a test", 88 | "https://github.com", 89 | } 90 | 91 | if !reflect.DeepEqual(got, want) { 92 | t.Errorf("Strings() = %v, expected %v", got, want) 93 | } 94 | } 95 | 96 | func TestLabels_strings(t *testing.T) { 97 | labels := Labels{ 98 | Label{ 99 | "foo", 100 | "FF0000", 101 | "a foo", 102 | "https://github.com", 103 | }, 104 | Label{ 105 | "bar", 106 | "00FF00", 107 | "a bar", 108 | "", 109 | }, 110 | } 111 | 112 | got := labels.strings() 113 | want := [][]string{ 114 | { 115 | "foo", 116 | "FF0000", 117 | "a foo", 118 | "https://github.com", 119 | }, 120 | { 121 | "bar", 122 | "00FF00", 123 | "a bar", 124 | "", 125 | }, 126 | } 127 | 128 | if !reflect.DeepEqual(got, want) { 129 | t.Errorf("Strings() = %v, expected %v", got, want) 130 | } 131 | } 132 | 133 | func TestLabels_write(t *testing.T) { 134 | labels := Labels{ 135 | Label{ 136 | "foo", 137 | "FF0000", 138 | "a foo", 139 | "https://github.com", 140 | }, 141 | Label{ 142 | "bar", 143 | "00FF00", 144 | "a bar", 145 | "", 146 | }, 147 | } 148 | 149 | tests := []struct { 150 | name string 151 | format OutputFormat 152 | want string 153 | wantE bool 154 | }{ 155 | { 156 | name: "csv", 157 | format: CSV, 158 | want: heredoc.Doc(`name,color,description,url 159 | foo,FF0000,a foo,https://github.com 160 | bar,00FF00,a bar, 161 | `), 162 | }, 163 | { 164 | name: "json", 165 | format: JSON, 166 | want: heredoc.Doc(`[ 167 | { 168 | "name": "foo", 169 | "color": "FF0000", 170 | "description": "a foo", 171 | "url": "https://github.com" 172 | }, 173 | { 174 | "name": "bar", 175 | "color": "00FF00", 176 | "description": "a bar" 177 | } 178 | ] 179 | `), 180 | }, 181 | { 182 | name: "unknown", 183 | format: "unknown", 184 | wantE: true, 185 | }, 186 | } 187 | 188 | for _, tt := range tests { 189 | t.Run(tt.name, func(t *testing.T) { 190 | bytes := &bytes.Buffer{} 191 | if err := labels.Write(tt.format, bytes); (err != nil) != tt.wantE { 192 | t.Errorf("Write() error = %v, expected error %v", err, tt.wantE) 193 | } else if bytes.String() != tt.want { 194 | t.Errorf("Write() = %v, expected %v", bytes.String(), tt.want) 195 | } 196 | }) 197 | } 198 | } 199 | 200 | func TestReadLabel(t *testing.T) { 201 | tests := []struct { 202 | name string 203 | data []string 204 | want Label 205 | wantE bool 206 | }{ 207 | { 208 | name: "too few fields", 209 | data: []string{"1", "2", "3"}, 210 | wantE: true, 211 | }, 212 | { 213 | name: "too many fields", 214 | data: []string{"1", "2", "3", "4", "right out"}, 215 | wantE: true, 216 | }, 217 | { 218 | name: "just right", 219 | data: []string{"1", "2", "3", "4"}, 220 | want: Label{ 221 | Name: "1", 222 | Color: "2", 223 | Description: "3", 224 | URL: "4", 225 | }, 226 | }, 227 | } 228 | 229 | for _, tt := range tests { 230 | t.Run(tt.name, func(t *testing.T) { 231 | if got, err := readLabel(tt.data); (err != nil) != tt.wantE { 232 | t.Errorf("readLabel() error = %v, expected %v", err, tt.wantE) 233 | } else if got != nil && !reflect.DeepEqual(*got, tt.want) { 234 | t.Errorf("readLabel() = %v, expected %v", *got, tt.want) 235 | } 236 | }) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /internal/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type keyStore interface { 12 | get(key string) string 13 | } 14 | 15 | type GlobalOptions struct { 16 | owner string 17 | repo string 18 | 19 | // test 20 | keys keyStore 21 | } 22 | 23 | func New(cmd *cobra.Command) *GlobalOptions { 24 | opts := &GlobalOptions{} 25 | cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { 26 | cmd.SilenceUsage = true 27 | 28 | repoOverride, _ := cmd.Flags().GetString("repo") 29 | return opts.parseRepoOverride(repoOverride) 30 | } 31 | 32 | cmd.PersistentFlags().StringP("repo", "R", "", "Select another repository using the `OWNER/REPO` format") 33 | 34 | return opts 35 | } 36 | 37 | func (opts *GlobalOptions) Repo() (owner, repo string) { 38 | return opts.owner, opts.repo 39 | } 40 | 41 | func (opts *GlobalOptions) parseRepoOverride(repoOverride string) error { 42 | if len(repoOverride) == 0 { 43 | if opts.keys == nil { 44 | opts.keys = &environment{} 45 | } 46 | repoOverride = opts.keys.get("GH_REPO") 47 | } 48 | 49 | if len(repoOverride) == 0 { 50 | opts.owner = ":owner" 51 | opts.repo = ":repo" 52 | return nil 53 | } 54 | 55 | parts := strings.Split(repoOverride, "/") 56 | 57 | if len(parts) != 2 { 58 | return fmt.Errorf(`expected the "OWNER/REPO" format, got %s`, repoOverride) 59 | } 60 | 61 | for _, part := range parts { 62 | if part == "" { 63 | return fmt.Errorf(`expected the "OWNER/REPO" format, got %s`, repoOverride) 64 | } 65 | } 66 | 67 | opts.owner = parts[0] 68 | opts.repo = parts[1] 69 | return nil 70 | } 71 | 72 | type environment struct{} 73 | 74 | func (env *environment) get(key string) string { 75 | return os.Getenv(key) 76 | } 77 | -------------------------------------------------------------------------------- /internal/options/options_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "testing" 4 | 5 | func Test_RepoOverride(t *testing.T) { 6 | opts := GlobalOptions{ 7 | owner: "heaths", 8 | repo: "gh-label", 9 | } 10 | 11 | if owner, repo := opts.Repo(); owner != "heaths" || repo != "gh-label" { 12 | t.Errorf(`RepoOverride() = (%s, %s); want: ("heaths", "gh-label")`, owner, repo) 13 | } 14 | } 15 | 16 | func Test_parseRepoOverride(t *testing.T) { 17 | type args struct { 18 | args string 19 | env map[string]string 20 | } 21 | 22 | type want struct { 23 | owner string 24 | repo string 25 | } 26 | 27 | tests := []struct { 28 | name string 29 | args args 30 | want want 31 | wantE bool 32 | }{ 33 | { 34 | name: "empty", 35 | want: want{ 36 | owner: ":owner", 37 | repo: ":repo", 38 | }, 39 | }, 40 | { 41 | name: "from environment", 42 | args: args{ 43 | env: map[string]string{ 44 | "GH_REPO": "heaths/gh-label", 45 | }, 46 | }, 47 | want: want{ 48 | owner: "heaths", 49 | repo: "gh-label", 50 | }, 51 | }, 52 | { 53 | name: "too few slashes", 54 | args: args{ 55 | args: "heaths", 56 | }, 57 | wantE: true, 58 | }, 59 | { 60 | name: "too many slashes", 61 | args: args{ 62 | args: "github.com/heaths/gh-label", 63 | }, 64 | wantE: true, 65 | }, 66 | { 67 | name: "empty parts", 68 | args: args{ 69 | args: "/", 70 | }, 71 | wantE: true, 72 | }, 73 | } 74 | 75 | for _, tt := range tests { 76 | t.Run(tt.name, func(t *testing.T) { 77 | opts := GlobalOptions{ 78 | keys: &mockStore{ 79 | env: tt.args.env, 80 | }, 81 | } 82 | 83 | if err := opts.parseRepoOverride(tt.args.args); (err != nil) != tt.wantE { 84 | t.Errorf("parseRepoOverride() = %v, wantE %v", err, tt.wantE) 85 | return 86 | } 87 | 88 | if opts.owner != tt.want.owner { 89 | t.Errorf("parseRepoOverride() owner = %q, want %q", opts.owner, tt.want.owner) 90 | return 91 | } 92 | 93 | if opts.repo != tt.want.repo { 94 | t.Errorf("parseRepoOverride() repo = %q, want %q", opts.repo, tt.want.repo) 95 | } 96 | }) 97 | } 98 | } 99 | 100 | type mockStore struct { 101 | env map[string]string 102 | } 103 | 104 | func (m *mockStore) get(key string) string { 105 | return m.env[key] 106 | } 107 | -------------------------------------------------------------------------------- /internal/utils/colors.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | func RandomColor() string { 11 | r := rand.Int31n(256) 12 | g := rand.Int31n(256) 13 | b := rand.Int31n(256) 14 | 15 | return fmt.Sprintf("%02X%02X%02X", r, g, b) 16 | } 17 | 18 | func ValidateColor(s string) (string, error) { 19 | if match, _ := regexp.MatchString("^#?[A-Fa-f0-9]{6}$", s); !match { 20 | return "", fmt.Errorf(`colors must include 6 hexadecimal digits for RGB with optional "#" prefix`) 21 | } 22 | 23 | if strings.HasPrefix(s, "#") { 24 | return s[1:], nil 25 | } 26 | 27 | return s, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/utils/colors_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | func Test_RandomColor(t *testing.T) { 9 | t.Run("validate random colors", func(t *testing.T) { 10 | re := regexp.MustCompile("^[A-Z0-9]{6}$") 11 | for i := 0; i < 10; i++ { 12 | color := RandomColor() 13 | if !re.MatchString(color) { 14 | t.Errorf("RandomColor() = %s, want pattern: %s", color, re.String()) 15 | } 16 | } 17 | }) 18 | } 19 | 20 | // cSpell:ignore aabbzz 21 | func Test_ValidateColor(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | color string 25 | want string 26 | wantE bool 27 | }{ 28 | { 29 | name: "valid color", 30 | color: "11bb33", 31 | want: "11bb33", 32 | }, 33 | { 34 | name: "valid color with # prefix", 35 | color: "#11bb33", 36 | want: "11bb33", 37 | }, 38 | { 39 | name: "color too short", 40 | color: "1b3", 41 | wantE: true, 42 | }, 43 | { 44 | name: "color too long", 45 | color: "11bb33dd", 46 | wantE: true, 47 | }, 48 | { 49 | name: "invalid hex digit", 50 | color: "aabbzz", 51 | wantE: true, 52 | }, 53 | } 54 | 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(t *testing.T) { 57 | if got, err := ValidateColor(tt.color); (err != nil) != tt.wantE { 58 | t.Errorf("ValidateColor() error = %v, wantE: %v", err, tt.wantE) 59 | } else if got != tt.want { 60 | t.Errorf("ValidateColor() color = %s, want: %s", got, tt.want) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func AreEqualStrings(a, b []string) bool { 4 | if len(a) != len(b) { 5 | return false 6 | } 7 | 8 | for i, e := range a { 9 | if e != b[i] { 10 | return false 11 | } 12 | } 13 | 14 | return true 15 | } 16 | -------------------------------------------------------------------------------- /internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestAreEqualStrings(t *testing.T) { 9 | tests := []struct { 10 | a []string 11 | b []string 12 | want bool 13 | }{ 14 | { 15 | a: nil, 16 | b: nil, 17 | want: true, 18 | }, 19 | { 20 | a: []string{}, 21 | b: []string{}, 22 | want: true, 23 | }, 24 | { 25 | a: []string{"x"}, 26 | b: []string{"x"}, 27 | want: true, 28 | }, 29 | { 30 | a: []string{"x", "y"}, 31 | b: []string{"x", "y"}, 32 | want: true, 33 | }, 34 | { 35 | a: []string{"x"}, 36 | b: []string{"y"}, 37 | want: false, 38 | }, 39 | { 40 | a: []string{"x"}, 41 | b: []string{"x", "y"}, 42 | want: false, 43 | }, 44 | { 45 | a: []string{"x"}, 46 | b: nil, 47 | want: false, 48 | }, 49 | { 50 | a: []string{"x"}, 51 | b: []string{}, 52 | want: false, 53 | }, 54 | { 55 | a: []string{"y"}, 56 | b: []string{"x"}, 57 | want: false, 58 | }, 59 | { 60 | a: []string{"x", "y"}, 61 | b: []string{"x"}, 62 | want: false, 63 | }, 64 | { 65 | a: nil, 66 | b: []string{"x"}, 67 | want: false, 68 | }, 69 | { 70 | a: []string{}, 71 | b: []string{"x"}, 72 | want: false, 73 | }, 74 | } 75 | 76 | for _, tt := range tests { 77 | t.Run(fmt.Sprintf("a = %v, b = %v", tt.a, tt.b), func(t *testing.T) { 78 | if got := AreEqualStrings(tt.a, tt.b); got != tt.want { 79 | t.Errorf("AreEqualStrings() = %v, want %v", got, tt.want) 80 | } 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/heaths/gh-label/internal/cmd/create" 7 | "github.com/heaths/gh-label/internal/cmd/delete" 8 | "github.com/heaths/gh-label/internal/cmd/edit" 9 | "github.com/heaths/gh-label/internal/cmd/export" 10 | importcmd "github.com/heaths/gh-label/internal/cmd/import" 11 | "github.com/heaths/gh-label/internal/cmd/list" 12 | "github.com/heaths/gh-label/internal/options" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func main() { 17 | rootCmd := cobra.Command{ 18 | Use: "label", 19 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 20 | cmd.SilenceUsage = true 21 | }, 22 | } 23 | 24 | opts := options.New(&rootCmd) 25 | 26 | rootCmd.AddCommand(create.CreateCmd(opts)) 27 | rootCmd.AddCommand(delete.DeleteCmd(opts)) 28 | rootCmd.AddCommand(edit.EditCmd(opts)) 29 | rootCmd.AddCommand(export.ExportCmd(opts)) 30 | rootCmd.AddCommand(importcmd.ImportCmd(opts)) 31 | rootCmd.AddCommand(list.ListCmd(opts)) 32 | 33 | if err := rootCmd.Execute(); err != nil { 34 | os.Exit(1) 35 | } 36 | } 37 | --------------------------------------------------------------------------------