Merge branch 'trunk' into jg/event-handling

This commit is contained in:
Bernardo 2022-03-16 20:51:47 +00:00 committed by GitHub
commit 2e3400a8af
110 changed files with 4619 additions and 1014 deletions

View file

@ -0,0 +1,5 @@
{
"extensions": [
"golang.go"
]
}

View file

@ -10,13 +10,18 @@ on:
schedule:
- cron: "0 0 * * 0"
permissions:
actions: read # for github/codeql-action/init to get workflow details
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/analyze to upload SARIF results
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v1

View file

@ -1,5 +1,9 @@
name: Tests
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
strategy:
@ -15,16 +19,15 @@ jobs:
go-version: 1.16
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Cache Go modules
- name: Restore Go modules cache
uses: actions/cache@v2
with:
path: ~/go
key: ${{ runner.os }}-build-${{ hashFiles('go.mod') }}
path: ~/go/pkg/mod
key: go-${{ runner.os }}-${{ hashFiles('go.mod') }}
restore-keys: |
${{ runner.os }}-build-
${{ runner.os }}-
go-${{ runner.os }}-
- name: Download dependencies
run: go mod download

View file

@ -2,16 +2,21 @@ name: Issue Automation
on:
issues:
types: [opened]
permissions:
contents: none
issues: write
jobs:
issue-auto:
runs-on: ubuntu-latest
steps:
- name: label incoming issue
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }}
ISSUENUM: ${{ github.event.issue.number }}
ISSUEAUTHOR: ${{ github.event.issue.user.login }}
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.AUTOMATION_TOKEN }}
ISSUENUM: ${{ github.event.issue.number }}
ISSUEAUTHOR: ${{ github.event.issue.user.login }}
run: |
if ! gh api orgs/cli/public_members/$ISSUEAUTHOR --silent 2>/dev/null
then

View file

@ -11,6 +11,9 @@ on:
- go.mod
- go.sum
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
@ -22,14 +25,22 @@ jobs:
go-version: 1.16
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Restore Go modules cache
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: go-${{ runner.os }}-${{ hashFiles('go.mod') }}
restore-keys: |
go-${{ runner.os }}-
- name: Verify dependencies
run: |
go mod verify
go mod download
LINT_VERSION=1.39.0
LINT_VERSION=1.44.2
curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \
tar xz --strip-components 1 --wildcards \*/golangci-lint
mkdir -p bin && mv golangci-lint bin/

View file

@ -2,6 +2,12 @@ name: PR Automation
on:
pull_request_target:
types: [ready_for_review, opened, reopened]
permissions:
contents: none
issues: write
pull-requests: write
jobs:
pr-auto:
runs-on: ubuntu-latest

View file

@ -5,12 +5,16 @@ on:
tags:
- "v*"
permissions:
contents: write # publishing releases
repository-projects: write # move cards between columns
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
@ -27,6 +31,13 @@ jobs:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Install osslsigncode
run: sudo apt-get install -y osslsigncode
- name: Obtain signing cert
run: |
cert="$(mktemp -t cert.XXX)"
base64 -d <<<"$CERT_CONTENTS" > "$cert"
echo "CERT_FILE=$cert" >> $GITHUB_ENV
env:
CERT_CONTENTS: ${{ secrets.WINDOWS_CERT_PFX }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
@ -35,10 +46,9 @@ jobs:
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
GORELEASER_CURRENT_TAG: ${{steps.changelog.outputs.tag-name}}
GITHUB_CERT_PASSWORD: ${{secrets.GITHUB_CERT_PASSWORD}}
DESKTOP_CERT_TOKEN: ${{secrets.DESKTOP_CERT_TOKEN}}
CERT_PASSWORD: ${{secrets.WINDOWS_CERT_PASSWORD}}
- name: Checkout documentation site
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: github/cli.github.com
path: site
@ -122,7 +132,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download gh.exe
id: download_exe
shell: bash
@ -147,15 +157,18 @@ jobs:
"${MSBUILD_PATH}\MSBuild.exe" ./build/windows/gh.wixproj -p:SourceDir="$PWD" -p:OutputPath="$PWD" -p:OutputName="$name" -p:ProductVersion="$version"
- name: Obtain signing cert
id: obtain_cert
shell: bash
run: |
base64 -d <<<"$CERT_CONTENTS" > ./cert.pfx
printf "::set-output name=cert-file::%s\n" ".\\cert.pfx"
env:
DESKTOP_CERT_TOKEN: ${{ secrets.DESKTOP_CERT_TOKEN }}
run: .\script\setup-windows-certificate.ps1
CERT_CONTENTS: ${{ secrets.WINDOWS_CERT_PFX }}
- name: Sign MSI
env:
CERT_FILE: ${{ steps.obtain_cert.outputs.cert-file }}
EXE_FILE: ${{ steps.buildmsi.outputs.msi }}
GITHUB_CERT_PASSWORD: ${{ secrets.GITHUB_CERT_PASSWORD }}
run: .\script\sign.ps1 -Certificate $env:CERT_FILE -Executable $env:EXE_FILE
CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
run: .\script\signtool sign /d "GitHub CLI" /f $env:CERT_FILE /p $env:CERT_PASSWORD /fd sha256 /tr http://timestamp.digicert.com /v $env:EXE_FILE
- name: Upload MSI
shell: bash
run: |
@ -179,7 +192,7 @@ jobs:
env:
COMMITTER_TOKEN: ${{ secrets.UPLOAD_GITHUB_TOKEN }}
- name: Checkout scoop bucket
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: cli/scoop-gh
path: scoop-gh

View file

@ -9,7 +9,6 @@ before:
hooks:
- go mod tidy
- make manpages GH_VERSION={{.Version}}
- ./script/prepare-windows-cert.sh '{{ if index .Env "GITHUB_CERT_PASSWORD" }}{{ .Env.GITHUB_CERT_PASSWORD}}{{ end }}' '{{ if index .Env "DESKTOP_CERT_TOKEN" }}{{ .Env.DESKTOP_CERT_TOKEN}}{{ end }}'
builds:
- <<: &build_defaults

View file

@ -236,7 +236,23 @@ func ScopesSuggestion(resp *http.Response) string {
for _, s := range strings.Split(tokenHasScopes, ",") {
s = strings.TrimSpace(s)
gotScopes[s] = struct{}{}
if strings.HasPrefix(s, "admin:") {
// Certain scopes may be grouped under a single "top-level" scope. The following branch
// statements include these grouped/implied scopes when the top-level scope is encountered.
// See https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps.
if s == "repo" {
gotScopes["repo:status"] = struct{}{}
gotScopes["repo_deployment"] = struct{}{}
gotScopes["public_repo"] = struct{}{}
gotScopes["repo:invite"] = struct{}{}
gotScopes["security_events"] = struct{}{}
} else if s == "user" {
gotScopes["read:user"] = struct{}{}
gotScopes["user:email"] = struct{}{}
gotScopes["user:follow"] = struct{}{}
} else if s == "codespace" {
gotScopes["codespace:secrets"] = struct{}{}
} else if strings.HasPrefix(s, "admin:") {
gotScopes["read:"+strings.TrimPrefix(s, "admin:")] = struct{}{}
gotScopes["write:"+strings.TrimPrefix(s, "admin:")] = struct{}{}
} else if strings.HasPrefix(s, "write:") {

View file

@ -5,13 +5,14 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/cli/cli/v2/internal/ghinstance"
"io"
"net/http"
"sort"
"strings"
"time"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/shurcooL/githubv4"
)
@ -45,6 +46,7 @@ type Repository struct {
MergeCommitAllowed bool
SquashMergeAllowed bool
RebaseMergeAllowed bool
AutoMergeAllowed bool
ForkCount int
StargazerCount int
@ -67,6 +69,7 @@ type Repository struct {
IsArchived bool
IsEmpty bool
IsFork bool
ForkingAllowed bool
IsInOrganization bool
IsMirror bool
IsPrivate bool
@ -80,6 +83,7 @@ type Repository struct {
ViewerPermission string
ViewerPossibleCommitEmails []string
ViewerSubscription string
Visibility string
RepositoryTopics struct {
Nodes []struct {

View file

@ -14,8 +14,8 @@ our release schedule.
Install:
```bash
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/etc/apt/trusted.gpg.d/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh
```
@ -29,13 +29,19 @@ sudo apt install gh
### Fedora, CentOS, Red Hat Enterprise Linux (dnf)
Install:
Install from our package repository for immediate access to latest releases:
```bash
sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
sudo dnf install gh
```
Alternatively, install from the [community repository](https://packages.fedoraproject.org/pkgs/gh/gh/):
```bash
sudo dnf install gh
```
Upgrade:
```bash

View file

@ -6,31 +6,23 @@ triage role. The initial expectation is that the person in the role for the week
## Expectations for incoming issues
All incoming issues need either an **enhancement**, **bug**, or **docs** label.
All incoming issues need either an `enhancement`, `bug`, or `docs` label.
To be considered triaged, **enhancement** issues require at least one of the following additional labels:
To be considered triaged, `enhancement` issues require at least one of the following additional labels:
- **core**: work reserved for the core CLI team
- **help wanted**: work that we would accept contributions for
- **needs-design**: work that requires input from a UX designer before it can move forward
- **needs-investigation**: work that requires a mystery be solved by the core team before it can move forward
- **needs-user-input**: work that requires more information from the reporter before it can move forward
- `core`: reserved for the core CLI team
- `help wanted`: signal that we are accepting contributions for this
- `discuss`: add to our team's queue to discuss during a sync
- `needs-investigation`: work that requires a mystery be solved by the core team before it can move forward
- `needs-user-input`: we need more information from our users before the task can move forward
To be considered triaged, **bug** issues require a severity label: one of **p1**, **p2**, or **p3**
For a more detailed breakdown of **how** to triage an issue, see the _Issue triage flowchart_ below.
To be considered triaged, `bug` issues require a severity label: one of `p1`, `p2`, or `p3`
## Expectations for community pull requests
To be considered triaged, incoming pull requests should:
- be checked for a corresponding **help wanted** issue
- be checked for basic quality: are the builds passing? have tests been added?
- be checked for redundancy: is there already a PR dealing with this?
Once a pull request has been triaged, it should be moved to the **Needs Review** column of the project board.
For a more detailed breakdown of **how** to triage an issue, see the _PR triage flowchart_ below.
All incoming pull requests are assigned to one of the engineers for review on a round-robin basis.
The person in a triage role for a week could take a glance at these pull requests, mostly to see whether
the changeset is feasible and to allow the associated CI run for new contributors.
## Issue triage flowchart
@ -46,30 +38,17 @@ For a more detailed breakdown of **how** to triage an issue, see the _PR triage
- add `help wanted` label
- consider adding `good first issue` label
- do we want to do it?
- comment acknowledging it
- comment acknowledging that
- add `core` label
- add to project TODO column if this is something that should ship soon
- add to the project TODO column if this is something that should ship soon
- is it intriguing, but requires discussion?
- label `needs-design` if design input is needed, ping
- label `discuss`
- label `needs-investigation` if engineering research is required before action can be taken
- does it need more info from the issue author?
- ask the user for details
- add `needs-user-input` label
- is it a usage/support question?
- offer some instructions/workaround and close
## Pull request triage flowchart
- can it be closed outright?
- e.g. spam/junk
- close
- do we not want to do it?
- comment and close
- is it intriguing, but requires discussion and there is no referenced issue?
- request an issue
- close
- is it something we want to include?
- add to `needs review` column
- consider converting the Issue to a Discussion
## Weekly PR audit

View file

@ -307,6 +307,15 @@ func CheckoutBranch(branch string) error {
return run.PrepareCmd(configCmd).Run()
}
func CheckoutNewBranch(remoteName, branch string) error {
track := fmt.Sprintf("%s/%s", remoteName, branch)
configCmd, err := GitCommand("checkout", "-b", branch, "--track", track)
if err != nil {
return err
}
return run.PrepareCmd(configCmd).Run()
}
// pull changes from remote branch without version history
func Pull(remote, branch string) error {
pullCmd, err := GitCommand("pull", "--ff-only", remote, branch)

11
go.mod
View file

@ -20,7 +20,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0
github.com/henvic/httpretty v0.0.6
github.com/itchyny/gojq v0.12.6
github.com/itchyny/gojq v0.12.7
github.com/joho/godotenv v1.4.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-colorable v0.1.12
@ -33,13 +33,14 @@ require (
github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect
github.com/sourcegraph/jsonrpc2 v0.1.0
github.com/spf13/cobra v1.3.0
github.com/spf13/cobra v1.4.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.7.1
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

404
go.sum
View file

@ -13,20 +13,6 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -35,7 +21,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@ -50,44 +35,22 @@ github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO
github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY=
github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/glamour v0.4.0 h1:scR+smyB7WdmrlIaff6IVlm48P48JaNM7JypM/VGl4k=
github.com/charmbracelet/glamour v0.4.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
github.com/cli/browser v1.1.0 h1:xOZBfkfY9L9vMBgqb1YwRirGu6QFaQ5dP/vXt5ENSOY=
github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI=
@ -101,17 +64,6 @@ github.com/cli/shurcooL-graphql v0.0.1 h1:/9J3t9O6p1B8zdBBtQighq5g7DQRItBwuwGh3S
github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
@ -124,38 +76,18 @@ github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -163,8 +95,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -177,13 +107,8 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -193,18 +118,11 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@ -212,94 +130,44 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs=
github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/itchyny/gojq v0.12.6 h1:VjaFn59Em2wTxDNGcrRkDK9ZHMNa8IksOgL13sLL4d0=
github.com/itchyny/gojq v0.12.6/go.mod h1:ZHrkfu7A+RbZLy5J1/JKpS4poEqrzItSTGDItqsfP0A=
github.com/itchyny/gojq v0.12.7 h1:hYPTpeWfrJ1OT+2j6cvBScbhl0TkdwGM4bc66onUSOQ=
github.com/itchyny/gojq v0.12.7/go.mod h1:ZdvNHVlzPgUf8pgjnuDTmGfHA/21KoutQUJ3An/xNuw=
github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU=
github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -307,20 +175,10 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@ -328,123 +186,60 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo=
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b h1:0/ecDXh/HTHRtSDSFnD2/Ta1yQ5J76ZspVY4u0/jGFk=
github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU=
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sourcegraph/jsonrpc2 v0.1.0 h1:ohJHjZ+PcaLxDUjqk2NC3tIGsVa5bXThe1ZheSXOjuk=
github.com/sourcegraph/jsonrpc2 v0.1.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.4 h1:zNWRjYUW32G9KirMXYHQHVNFkXvMI7LpgNW2AgYAoIs=
github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -467,8 +262,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
@ -477,25 +270,17 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -510,39 +295,19 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -551,35 +316,24 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -591,46 +345,24 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -650,7 +382,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -675,22 +406,9 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -712,30 +430,13 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -759,46 +460,12 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -811,22 +478,6 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -836,24 +487,13 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=

View file

@ -52,6 +52,13 @@ const (
vscsAPI = "https://online.visualstudio.com"
)
const (
VSCSTargetLocal = "local"
VSCSTargetDevelopment = "development"
VSCSTargetPPE = "ppe"
VSCSTargetProduction = "production"
)
// API is the interface to the codespace service.
type API struct {
client httpClient
@ -160,16 +167,22 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error
}
// Codespace represents a codespace.
// You can see more about the fields in this type in the codespaces api docs:
// https://docs.github.com/en/rest/reference/codespaces
type Codespace struct {
Name string `json:"name"`
CreatedAt string `json:"created_at"`
DisplayName string `json:"display_name"`
LastUsedAt string `json:"last_used_at"`
Owner User `json:"owner"`
Repository Repository `json:"repository"`
State string `json:"state"`
GitStatus CodespaceGitStatus `json:"git_status"`
Connection CodespaceConnection `json:"connection"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
DisplayName string `json:"display_name"`
LastUsedAt string `json:"last_used_at"`
Owner User `json:"owner"`
Repository Repository `json:"repository"`
State string `json:"state"`
GitStatus CodespaceGitStatus `json:"git_status"`
Connection CodespaceConnection `json:"connection"`
Machine CodespaceMachine `json:"machine"`
VSCSTarget string `json:"vscs_target"`
PendingOperation bool `json:"pending_operation"`
PendingOperationDisabledReason string `json:"pending_operation_disabled_reason"`
}
type CodespaceGitStatus struct {
@ -180,6 +193,15 @@ type CodespaceGitStatus struct {
HasUncommitedChanges bool `json:"has_uncommited_changes"`
}
type CodespaceMachine struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
OperatingSystem string `json:"operating_system"`
StorageInBytes int `json:"storage_in_bytes"`
MemoryInBytes int `json:"memory_in_bytes"`
CPUCount int `json:"cpus"`
}
const (
// CodespaceStateAvailable is the state for a running codespace environment.
CodespaceStateAvailable = "Available"
@ -207,6 +229,8 @@ var CodespaceFields = []string{
"gitStatus",
"createdAt",
"lastUsedAt",
"machineName",
"vscsTarget",
}
func (c *Codespace) ExportData(fields []string) map[string]interface{} {
@ -219,12 +243,18 @@ func (c *Codespace) ExportData(fields []string) map[string]interface{} {
data[f] = c.Owner.Login
case "repository":
data[f] = c.Repository.FullName
case "machineName":
data[f] = c.Machine.Name
case "gitStatus":
data[f] = map[string]interface{}{
"ref": c.GitStatus.Ref,
"hasUnpushedChanges": c.GitStatus.HasUnpushedChanges,
"hasUncommitedChanges": c.GitStatus.HasUncommitedChanges,
}
case "vscsTarget":
if c.VSCSTarget != "" && c.VSCSTarget != VSCSTargetProduction {
data[f] = c.VSCSTarget
}
default:
sf := v.FieldByNameFunc(func(s string) bool {
return strings.EqualFold(f, s)
@ -265,6 +295,7 @@ func (a *API) ListCodespaces(ctx context.Context, limit int) (codespaces []*Code
var response struct {
Codespaces []*Codespace `json:"codespaces"`
}
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&response); err != nil {
return nil, fmt.Errorf("error unmarshaling response: %w", err)
@ -565,6 +596,9 @@ type CreateCodespaceParams struct {
Branch string
Machine string
Location string
VSCSTarget string
VSCSTargetURL string
PermissionsOptOut bool
}
// CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it
@ -610,10 +644,22 @@ type startCreateRequest struct {
Ref string `json:"ref"`
Location string `json:"location"`
Machine string `json:"machine"`
VSCSTarget string `json:"vscs_target,omitempty"`
VSCSTargetURL string `json:"vscs_target_url,omitempty"`
PermissionsOptOut bool `json:"devcontainer_permissions_opt_out"`
}
var errProvisioningInProgress = errors.New("provisioning in progress")
type AcceptPermissionsRequiredError struct {
Message string `json:"message"`
AllowPermissionsURL string `json:"allow_permissions_url"`
}
func (e AcceptPermissionsRequiredError) Error() string {
return e.Message
}
// startCreate starts the creation of a codespace.
// It may return success or an error, or errProvisioningInProgress indicating that the operation
// did not complete before the GitHub API's time limit for RPCs (10s), in which case the caller
@ -629,7 +675,11 @@ func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (*
Ref: params.Branch,
Location: params.Location,
Machine: params.Machine,
VSCSTarget: params.VSCSTarget,
VSCSTargetURL: params.VSCSTargetURL,
PermissionsOptOut: params.PermissionsOptOut,
})
if err != nil {
return nil, fmt.Errorf("error marshaling request: %w", err)
}
@ -648,6 +698,29 @@ func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (*
if resp.StatusCode == http.StatusAccepted {
return nil, errProvisioningInProgress // RPC finished before result of creation known
} else if resp.StatusCode == http.StatusUnauthorized {
var (
ue AcceptPermissionsRequiredError
bodyCopy = &bytes.Buffer{}
r = io.TeeReader(resp.Body, bodyCopy)
)
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("error reading response body: %w", err)
}
if err := json.Unmarshal(b, &ue); err != nil {
return nil, fmt.Errorf("error unmarshaling response: %w", err)
}
if ue.AllowPermissionsURL != "" {
return nil, ue
}
resp.Body = ioutil.NopCloser(bodyCopy)
return nil, api.HandleHTTPError(resp)
} else if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return nil, api.HandleHTTPError(resp)
}
@ -686,6 +759,70 @@ func (a *API) DeleteCodespace(ctx context.Context, codespaceName string) error {
return nil
}
type EditCodespaceParams struct {
DisplayName string `json:"display_name,omitempty"`
IdleTimeoutMinutes int `json:"idle_timeout_minutes,omitempty"`
Machine string `json:"machine,omitempty"`
}
func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *EditCodespaceParams) (*Codespace, error) {
requestBody, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("error marshaling request: %w", err)
}
req, err := http.NewRequest(http.MethodPatch, a.githubAPI+"/user/codespaces/"+codespaceName, bytes.NewBuffer(requestBody))
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
a.setHeaders(req)
resp, err := a.do(ctx, req, "/user/codespaces")
if err != nil {
return nil, fmt.Errorf("error making request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// 422 (unprocessable entity) is likely caused by the codespace having a
// pending op, so we'll fetch the codespace to see if that's the case
// and return a more understandable error message.
if resp.StatusCode == http.StatusUnprocessableEntity {
pendingOp, reason, err := a.checkForPendingOperation(ctx, codespaceName)
// If there's an error or there's not a pending op, we want to let
// this fall through to the normal api.HandleHTTPError flow
if err == nil && pendingOp {
return nil, fmt.Errorf(
"codespace is disabled while it has a pending operation: %s",
reason,
)
}
}
return nil, api.HandleHTTPError(resp)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %w", err)
}
var response Codespace
if err := json.Unmarshal(b, &response); err != nil {
return nil, fmt.Errorf("error unmarshaling response: %w", err)
}
return &response, nil
}
func (a *API) checkForPendingOperation(ctx context.Context, codespaceName string) (bool, string, error) {
codespace, err := a.GetCodespace(ctx, codespaceName, false)
if err != nil {
return false, "", err
}
return codespace.PendingOperation, codespace.PendingOperationDisabledReason, nil
}
type getCodespaceRepositoryContentsResponse struct {
Content string `json:"content"`
}

View file

@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"strconv"
"testing"
)
@ -265,3 +266,210 @@ func TestRetries(t *testing.T) {
t.Fatalf("expected codespace name to be %q but got %q", csName, cs.Name)
}
}
func TestCodespace_ExportData(t *testing.T) {
type fields struct {
Name string
CreatedAt string
DisplayName string
LastUsedAt string
Owner User
Repository Repository
State string
GitStatus CodespaceGitStatus
Connection CodespaceConnection
Machine CodespaceMachine
}
type args struct {
fields []string
}
tests := []struct {
name string
fields fields
args args
want map[string]interface{}
}{
{
name: "just name",
fields: fields{
Name: "test",
},
args: args{
fields: []string{"name"},
},
want: map[string]interface{}{
"name": "test",
},
},
{
name: "just owner",
fields: fields{
Owner: User{
Login: "test",
},
},
args: args{
fields: []string{"owner"},
},
want: map[string]interface{}{
"owner": "test",
},
},
{
name: "just machine",
fields: fields{
Machine: CodespaceMachine{
Name: "test",
},
},
args: args{
fields: []string{"machineName"},
},
want: map[string]interface{}{
"machineName": "test",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Codespace{
Name: tt.fields.Name,
CreatedAt: tt.fields.CreatedAt,
DisplayName: tt.fields.DisplayName,
LastUsedAt: tt.fields.LastUsedAt,
Owner: tt.fields.Owner,
Repository: tt.fields.Repository,
State: tt.fields.State,
GitStatus: tt.fields.GitStatus,
Connection: tt.fields.Connection,
Machine: tt.fields.Machine,
}
if got := c.ExportData(tt.args.fields); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Codespace.ExportData() = %v, want %v", got, tt.want)
}
})
}
}
func createFakeEditServer(t *testing.T, codespaceName string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
checkPath := "/user/codespaces/" + codespaceName
if r.URL.Path != checkPath {
t.Fatal("Incorrect path")
}
if r.Method != http.MethodPatch {
t.Fatal("Incorrect method")
}
body := r.Body
if body == nil {
t.Fatal("No body")
}
defer body.Close()
var data map[string]interface{}
err := json.NewDecoder(body).Decode(&data)
if err != nil {
t.Fatal(err)
}
if data["display_name"] != "changeTo" {
t.Fatal("Incorrect display name")
}
response := Codespace{
DisplayName: "changeTo",
}
responseData, _ := json.Marshal(response)
fmt.Fprint(w, string(responseData))
}))
}
func TestAPI_EditCodespace(t *testing.T) {
type args struct {
ctx context.Context
codespaceName string
params *EditCodespaceParams
}
tests := []struct {
name string
args args
want *Codespace
wantErr bool
}{
{
name: "success",
args: args{
ctx: context.Background(),
codespaceName: "test",
params: &EditCodespaceParams{
DisplayName: "changeTo",
},
},
want: &Codespace{
DisplayName: "changeTo",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svr := createFakeEditServer(t, tt.args.codespaceName)
defer svr.Close()
a := &API{
client: &http.Client{},
githubAPI: svr.URL,
}
got, err := a.EditCodespace(tt.args.ctx, tt.args.codespaceName, tt.args.params)
if (err != nil) != tt.wantErr {
t.Errorf("API.EditCodespace() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("API.EditCodespace() = %v, want %v", got.DisplayName, tt.want.DisplayName)
}
})
}
}
func createFakeEditPendingOpServer(t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPatch {
w.WriteHeader(http.StatusUnprocessableEntity)
return
}
if r.Method == http.MethodGet {
response := Codespace{
PendingOperation: true,
PendingOperationDisabledReason: "Some pending operation",
}
responseData, _ := json.Marshal(response)
fmt.Fprint(w, string(responseData))
return
}
}))
}
func TestAPI_EditCodespacePendingOperation(t *testing.T) {
svr := createFakeEditPendingOpServer(t)
defer svr.Close()
a := &API{
client: &http.Client{},
githubAPI: svr.URL,
}
_, err := a.EditCodespace(context.Background(), "disabledCodespace", &EditCodespaceParams{DisplayName: "some silly name"})
if err == nil {
t.Error("Expected pending operation error, but got nothing")
}
if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" {
t.Errorf("Expected pending operation error, but got %v", err)
}
}

View file

@ -11,12 +11,10 @@ func NewCmdActions(f *cmdutil.Factory) *cobra.Command {
cs := f.IOStreams.ColorScheme()
cmd := &cobra.Command{
Use: "actions",
Short: "Learn about working with GitHub Actions",
Long: actionsExplainer(cs),
Annotations: map[string]string{
"IsActions": "true",
},
Use: "actions",
Short: "Learn about working with GitHub Actions",
Long: actionsExplainer(cs),
Hidden: true,
}
cmdutil.DisableAuthCheck(cmd)

View file

@ -24,8 +24,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
}
cmd := &cobra.Command{
Use: "list",
Short: "List your aliases",
Use: "list",
Short: "List your aliases",
Aliases: []string{"ls"},
Long: heredoc.Doc(`
This command prints out all of the aliases gh is configured to use.
`),

View file

@ -214,7 +214,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
cmd.Flags().StringArrayVarP(&opts.MagicFields, "field", "F", nil, "Add a typed parameter in `key=value` format")
cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter in `key=value` format")
cmd.Flags().StringArrayVarP(&opts.RequestHeaders, "header", "H", nil, "Add a HTTP request header in `key:value` format")
cmd.Flags().StringSliceVarP(&opts.Previews, "preview", "p", nil, "Opt into GitHub API previews")
cmd.Flags().StringSliceVarP(&opts.Previews, "preview", "p", nil, "GitHub API preview `names` to request (without the \"-preview\" suffix)")
cmd.Flags().BoolVarP(&opts.ShowResponseHeaders, "include", "i", false, "Include HTTP response headers in the output")
cmd.Flags().BoolVar(&opts.Paginate, "paginate", false, "Make additional HTTP requests to fetch all pages of results")
cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The `file` to use as body for the HTTP request (use \"-\" to read from standard input)")

View file

@ -14,8 +14,10 @@ import (
func NewCmdAuth(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "auth <command>",
Short: "Login, logout, and refresh your authentication",
Long: `Manage gh's authentication state.`,
Short: "Authenticate gh and git with GitHub",
Annotations: map[string]string{
"IsCore": "true",
},
}
cmdutil.DisableAuthCheck(cmd)

View file

@ -27,10 +27,11 @@ type LoginOptions struct {
Interactive bool
Hostname string
Scopes []string
Token string
Web bool
Hostname string
Scopes []string
Token string
Web bool
GitProtocol string
}
func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command {
@ -49,13 +50,17 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
Long: heredoc.Docf(`
Authenticate with a GitHub host.
The default authentication mode is a web-based browser flow.
The default authentication mode is a web-based browser flow. After completion, an
authentication token will be stored internally.
Alternatively, pass in a token on standard input by using %[1]s--with-token%[1]s.
Alternatively, use %[1]s--with-token%[1]s to pass in a token on standard input.
The minimum required scopes for the token are: "repo", "read:org".
The --scopes flag accepts a comma separated list of scopes you want your gh credentials to have. If
absent, this command ensures that gh has access to a minimum set of scopes.
Alternatively, gh will use the authentication token found in environment variables.
This method is most suitable for "headless" use of gh such as in automation. See
%[1]sgh help environment%[1]s for more info.
To use gh in GitHub Actions, add %[1]sGH_TOKEN: ${{secrets.GITHUB_TOKEN}}%[1]s to "env".
`, "`"),
Example: heredoc.Doc(`
# start interactive setup
@ -64,7 +69,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
# authenticate against github.com by reading the token from a file
$ gh auth login --with-token < mytoken.txt
# authenticate with a specific GitHub Enterprise Server instance
# authenticate with a specific GitHub instance
$ gh auth login --hostname enterprise.internal
`),
RunE: func(cmd *cobra.Command, args []string) error {
@ -108,9 +113,10 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
}
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitHub instance to authenticate with")
cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes for gh to have")
cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes to request")
cmd.Flags().BoolVar(&tokenStdin, "with-token", false, "Read token from standard input")
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a browser to authenticate")
cmdutil.StringEnumFlag(cmd, &opts.GitProtocol, "git-protocol", "p", "", []string{"ssh", "https"}, "The protocol to use for git operations")
return cmd
}
@ -186,6 +192,7 @@ func loginRun(opts *LoginOptions) error {
Web: opts.Web,
Scopes: opts.Scopes,
Executable: opts.MainExecutable,
GitProtocol: opts.GitProtocol,
})
}

View file

@ -29,6 +29,7 @@ type LoginOptions struct {
Web bool
Scopes []string
Executable string
GitProtocol string
sshContext sshContext
}
@ -39,8 +40,8 @@ func Login(opts *LoginOptions) error {
httpClient := opts.HTTPClient
cs := opts.IO.ColorScheme()
var gitProtocol string
if opts.Interactive {
gitProtocol := strings.ToLower(opts.GitProtocol)
if opts.Interactive && gitProtocol == "" {
var proto string
err := prompt.SurveyAskOne(&survey.Select{
Message: "What is your preferred protocol for Git operations?",

View file

@ -3,17 +3,11 @@ package codespace
import (
"context"
"fmt"
"io/ioutil"
"net/url"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
)
type browser interface {
Browse(string) error
}
func newCodeCmd(app *App) *cobra.Command {
var (
codespace string
@ -25,8 +19,7 @@ func newCodeCmd(app *App) *cobra.Command {
Short: "Open a codespace in Visual Studio Code",
Args: noArgsConstraint,
RunE: func(cmd *cobra.Command, args []string) error {
b := cmdutil.NewBrowser("", ioutil.Discard, app.io.ErrOut)
return app.VSCode(cmd.Context(), b, codespace, useInsiders)
return app.VSCode(cmd.Context(), codespace, useInsiders)
},
}
@ -37,20 +30,14 @@ func newCodeCmd(app *App) *cobra.Command {
}
// VSCode opens a codespace in the local VS VSCode application.
func (a *App) VSCode(ctx context.Context, browser browser, codespaceName string, useInsiders bool) error {
if codespaceName == "" {
codespace, err := chooseCodespace(ctx, a.apiClient)
if err != nil {
if err == errNoCodespaces {
return err
}
return fmt.Errorf("error choosing codespace: %w", err)
}
codespaceName = codespace.Name
func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool) error {
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
if err != nil {
return err
}
url := vscodeProtocolURL(codespaceName, useInsiders)
if err := browser.Browse(url); err != nil {
url := vscodeProtocolURL(codespace.Name, useInsiders)
if err := a.browser.Browse(url); err != nil {
return fmt.Errorf("error opening Visual Studio Code: %w", err)
}

View file

@ -4,7 +4,9 @@ import (
"context"
"testing"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
)
func TestApp_VSCode(t *testing.T) {
@ -40,11 +42,57 @@ func TestApp_VSCode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &cmdutil.TestBrowser{}
a := &App{}
if err := a.VSCode(context.Background(), b, tt.args.codespaceName, tt.args.useInsiders); (err != nil) != tt.wantErr {
a := &App{
browser: b,
apiClient: testCodeApiMock(),
}
if err := a.VSCode(context.Background(), tt.args.codespaceName, tt.args.useInsiders); (err != nil) != tt.wantErr {
t.Errorf("App.VSCode() error = %v, wantErr %v", err, tt.wantErr)
}
b.Verify(t, tt.wantURL)
})
}
}
func TestPendingOperationDisallowsCode(t *testing.T) {
app := testingCodeApp()
if err := app.VSCode(context.Background(), "disabledCodespace", false); err != nil {
if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" {
t.Errorf("expected pending operation error, but got: %v", err)
}
} else {
t.Error("expected pending operation error, but got nothing")
}
}
func testingCodeApp() *App {
io, _, _, _ := iostreams.Test()
return NewApp(io, nil, testCodeApiMock(), nil)
}
func testCodeApiMock() *apiClientMock {
user := &api.User{Login: "monalisa"}
testingCodespace := &api.Codespace{
Name: "monalisa-cli-cli-abcdef",
}
disabledCodespace := &api.Codespace{
Name: "disabledCodespace",
PendingOperation: true,
PendingOperationDisabledReason: "Some pending operation",
}
return &apiClientMock{
GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) {
if name == "disabledCodespace" {
return disabledCodespace, nil
}
return testingCodespace, nil
},
GetUserFunc: func(_ context.Context) (*api.User, error) {
return user, nil
},
AuthorizedKeysFunc: func(_ context.Context, _ string) ([]byte, error) {
return []byte{}, nil
},
}
}

View file

@ -21,6 +21,10 @@ import (
"golang.org/x/term"
)
type browser interface {
Browse(string) error
}
type executable interface {
Executable() string
}
@ -30,9 +34,10 @@ type App struct {
apiClient apiClient
errLogger *log.Logger
executable executable
browser browser
}
func NewApp(io *iostreams.IOStreams, exe executable, apiClient apiClient) *App {
func NewApp(io *iostreams.IOStreams, exe executable, apiClient apiClient, browser browser) *App {
errLogger := log.New(io.ErrOut, "", 0)
return &App{
@ -40,6 +45,7 @@ func NewApp(io *iostreams.IOStreams, exe executable, apiClient apiClient) *App {
apiClient: apiClient,
errLogger: errLogger,
executable: exe,
browser: browser,
}
}
@ -62,6 +68,7 @@ type apiClient interface {
StartCodespace(ctx context.Context, name string) error
StopCodespace(ctx context.Context, name string) error
CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error)
EditCodespace(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error)
GetRepository(ctx context.Context, nwo string) (*api.Repository, error)
AuthorizedKeys(ctx context.Context, user string) ([]byte, error)
GetCodespaceRegionLocation(ctx context.Context) (string, error)
@ -176,6 +183,13 @@ func getOrChooseCodespace(ctx context.Context, apiClient apiClient, codespaceNam
}
}
if codespace.PendingOperation {
return nil, fmt.Errorf(
"codespace is disabled while it has a pending operation: %s",
codespace.PendingOperationDisabledReason,
)
}
return codespace, nil
}

View file

@ -4,20 +4,24 @@ import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/cli/cli/v2/internal/codespaces"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
type createOptions struct {
repo string
branch string
machine string
showStatus bool
idleTimeout time.Duration
repo string
branch string
machine string
showStatus bool
permissionsOptOut bool
idleTimeout time.Duration
}
func newCreateCmd(app *App) *cobra.Command {
@ -35,6 +39,7 @@ func newCreateCmd(app *App) *cobra.Command {
createCmd.Flags().StringVarP(&opts.repo, "repo", "r", "", "repository name with owner: user/repo")
createCmd.Flags().StringVarP(&opts.branch, "branch", "b", "", "repository branch")
createCmd.Flags().StringVarP(&opts.machine, "machine", "m", "", "hardware specifications for the VM")
createCmd.Flags().BoolVarP(&opts.permissionsOptOut, "default-permissions", "", false, "do not prompt to accept additional permissions requested by the codespace")
createCmd.Flags().BoolVarP(&opts.showStatus, "status", "s", false, "show status of post-create command and dotfiles")
createCmd.Flags().DurationVar(&opts.idleTimeout, "idle-timeout", 0, "allowed inactivity before codespace is stopped, e.g. \"10m\", \"1h\"")
@ -43,7 +48,13 @@ func newCreateCmd(app *App) *cobra.Command {
// Create creates a new Codespace
func (a *App) Create(ctx context.Context, opts createOptions) error {
locationCh := getLocation(ctx, a.apiClient)
// Overrides for Codespace developers to target test environments
vscsLocation := os.Getenv("VSCS_LOCATION")
vscsTarget := os.Getenv("VSCS_TARGET")
vscsTargetUrl := os.Getenv("VSCS_TARGET_URL")
locationCh := getLocation(ctx, vscsLocation, a.apiClient)
userInputs := struct {
Repository string
@ -108,17 +119,32 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
return errors.New("there are no available machine types for this repository")
}
a.StartProgressIndicatorWithLabel("Creating codespace")
codespace, err := a.apiClient.CreateCodespace(ctx, &api.CreateCodespaceParams{
createParams := &api.CreateCodespaceParams{
RepositoryID: repository.ID,
Branch: branch,
Machine: machine,
Location: locationResult.Location,
VSCSTarget: vscsTarget,
VSCSTargetURL: vscsTargetUrl,
IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()),
})
PermissionsOptOut: opts.permissionsOptOut,
}
a.StartProgressIndicatorWithLabel("Creating codespace")
codespace, err := a.apiClient.CreateCodespace(ctx, createParams)
a.StopProgressIndicator()
if err != nil {
return fmt.Errorf("error creating codespace: %w", err)
var aerr api.AcceptPermissionsRequiredError
if !errors.As(err, &aerr) || aerr.AllowPermissionsURL == "" {
return fmt.Errorf("error creating codespace: %w", err)
}
codespace, err = a.handleAdditionalPermissions(ctx, createParams, aerr.AllowPermissionsURL)
if err != nil {
// this error could be a cmdutil.SilentError (in the case that the user opened the browser) so we don't want to wrap it
return err
}
}
if opts.showStatus {
@ -131,6 +157,72 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
return nil
}
func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api.CreateCodespaceParams, allowPermissionsURL string) (*api.Codespace, error) {
var (
isInteractive = a.io.CanPrompt()
cs = a.io.ColorScheme()
displayURL = utils.DisplayURL(allowPermissionsURL)
)
fmt.Fprintf(a.io.ErrOut, "You must authorize or deny additional permissions requested by this codespace before continuing.\n")
if !isInteractive {
fmt.Fprintf(a.io.ErrOut, "%s in your browser to review and authorize additional permissions: %s\n", cs.Bold("Open this URL"), displayURL)
fmt.Fprintf(a.io.ErrOut, "Alternatively, you can run %q with the %q option to continue without authorizing additional permissions.\n", a.io.ColorScheme().Bold("create"), cs.Bold("--default-permissions"))
return nil, cmdutil.SilentError
}
choices := []string{
"Continue in browser to review and authorize additional permissions (Recommended)",
"Continue without authorizing additional permissions",
}
permsSurvey := []*survey.Question{
{
Name: "accept",
Prompt: &survey.Select{
Message: "What would you like to do?",
Options: choices,
Default: choices[0],
},
Validate: survey.Required,
},
}
var answers struct {
Accept string
}
if err := ask(permsSurvey, &answers); err != nil {
return nil, fmt.Errorf("error getting answers: %w", err)
}
// if the user chose to continue in the browser, open the URL
if answers.Accept == choices[0] {
fmt.Fprintln(a.io.ErrOut, "Please re-run the create request after accepting permissions in the browser.")
if err := a.browser.Browse(allowPermissionsURL); err != nil {
return nil, fmt.Errorf("error opening browser: %w", err)
}
// browser opened successfully but we do not know if they accepted the permissions
// so we must exit and wait for the user to attempt the create again
return nil, cmdutil.SilentError
}
// if the user chose to create the codespace without the permissions,
// we can continue with the create opting out of the additional permissions
createParams.PermissionsOptOut = true
a.StartProgressIndicatorWithLabel("Creating codespace")
codespace, err := a.apiClient.CreateCodespace(ctx, createParams)
a.StopProgressIndicator()
if err != nil {
return nil, fmt.Errorf("error creating codespace: %w", err)
}
return codespace, nil
}
// showStatus polls the codespace for a list of post create states and their status. It will keep polling
// until all states have finished. Once all states have finished, we poll once more to check if any new
// states have been introduced and stop polling otherwise.
@ -200,9 +292,18 @@ type locationResult struct {
Err error
}
// getLocation fetches the closest Codespace datacenter region/location to the user.
func getLocation(ctx context.Context, apiClient apiClient) <-chan locationResult {
// getLocation fetches the closest Codespace datacenter
// region/location to the user, unless the 'vscsLocationOverride' override is set
func getLocation(ctx context.Context, vscsLocationOverride string, apiClient apiClient) <-chan locationResult {
ch := make(chan locationResult, 1)
// Developer override is set, return the override
if vscsLocationOverride != "" {
ch <- locationResult{vscsLocationOverride, nil}
return ch
}
// Dynamically fetch the region location
go func() {
location, err := apiClient.GetCodespaceRegionLocation(ctx)
ch <- locationResult{location, err}

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
)
@ -18,7 +19,7 @@ func TestApp_Create(t *testing.T) {
name string
fields fields
opts createOptions
wantErr bool
wantErr error
wantStdout string
wantStderr string
}{
@ -69,6 +70,57 @@ func TestApp_Create(t *testing.T) {
},
wantStdout: "monalisa-dotfiles-abcd1234\n",
},
{
name: "create codespace that requires accepting additional permissions",
fields: fields{
apiClient: &apiClientMock{
GetCodespaceRegionLocationFunc: func(ctx context.Context) (string, error) {
return "EUROPE", nil
},
GetRepositoryFunc: func(ctx context.Context, nwo string) (*api.Repository, error) {
return &api.Repository{
ID: 1234,
FullName: nwo,
DefaultBranch: "main",
}, nil
},
GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch, location string) ([]*api.Machine, error) {
return []*api.Machine{
{
Name: "GIGA",
DisplayName: "Gigabits of a machine",
},
}, nil
},
CreateCodespaceFunc: func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) {
if params.Branch != "main" {
return nil, fmt.Errorf("got branch %q, want %q", params.Branch, "main")
}
if params.IdleTimeoutMinutes != 30 {
return nil, fmt.Errorf("idle timeout minutes was %v", params.IdleTimeoutMinutes)
}
return &api.Codespace{}, api.AcceptPermissionsRequiredError{
AllowPermissionsURL: "https://example.com/permissions",
}
},
GetCodespaceRepoSuggestionsFunc: func(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error) {
return nil, nil // We can't ask for suggestions without a terminal.
},
},
},
opts: createOptions{
repo: "monalisa/dotfiles",
branch: "",
machine: "GIGA",
showStatus: false,
idleTimeout: 30 * time.Minute,
},
wantErr: cmdutil.SilentError,
wantStderr: `You must authorize or deny additional permissions requested by this codespace before continuing.
Open this URL in your browser to review and authorize additional permissions: example.com/permissions
Alternatively, you can run "create" with the "--default-permissions" option to continue without authorizing additional permissions.
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -77,7 +129,7 @@ func TestApp_Create(t *testing.T) {
io: io,
apiClient: tt.fields.apiClient,
}
if err := a.Create(context.Background(), tt.opts); (err != nil) != tt.wantErr {
if err := a.Create(context.Background(), tt.opts); err != tt.wantErr {
t.Errorf("App.Create() error = %v, wantErr %v", err, tt.wantErr)
}
if got := stdout.String(); got != tt.wantStdout {

View file

@ -190,7 +190,7 @@ func TestDelete(t *testing.T) {
io, _, stdout, stderr := iostreams.Test()
io.SetStdinTTY(true)
io.SetStdoutTTY(true)
app := NewApp(io, nil, apiMock)
app := NewApp(io, nil, apiMock, nil)
err := app.Delete(context.Background(), opts)
if (err != nil) != tt.wantErr {
t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr)

64
pkg/cmd/codespace/edit.go Normal file
View file

@ -0,0 +1,64 @@
package codespace
import (
"context"
"fmt"
"time"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/spf13/cobra"
)
type editOptions struct {
codespaceName string
displayName string
idleTimeout time.Duration
machine string
}
func newEditCmd(app *App) *cobra.Command {
opts := editOptions{}
editCmd := &cobra.Command{
Use: "edit",
Short: "Edit a codespace",
Args: noArgsConstraint,
RunE: func(cmd *cobra.Command, args []string) error {
return app.Edit(cmd.Context(), opts)
},
}
editCmd.Flags().StringVarP(&opts.codespaceName, "codespace", "c", "", "Name of the codespace")
editCmd.Flags().StringVarP(&opts.displayName, "displayName", "d", "", "display name")
editCmd.Flags().DurationVar(&opts.idleTimeout, "idle-timeout", 0, "allowed inactivity before codespace is stopped, e.g. \"10m\", \"1h\"")
editCmd.Flags().StringVarP(&opts.machine, "machine", "m", "", "hardware specifications for the VM")
return editCmd
}
// Edits a codespace
func (a *App) Edit(ctx context.Context, opts editOptions) error {
userInputs := struct {
CodespaceName string
DisplayName string
IdleTimeout time.Duration
SKU string
}{
CodespaceName: opts.codespaceName,
DisplayName: opts.displayName,
IdleTimeout: opts.idleTimeout,
SKU: opts.machine,
}
a.StartProgressIndicatorWithLabel("Editing codespace")
_, err := a.apiClient.EditCodespace(ctx, userInputs.CodespaceName, &api.EditCodespaceParams{
DisplayName: userInputs.DisplayName,
IdleTimeoutMinutes: int(userInputs.IdleTimeout.Minutes()),
Machine: userInputs.SKU,
})
a.StopProgressIndicator()
if err != nil {
return fmt.Errorf("error editing codespace: %w", err)
}
return nil
}

View file

@ -16,9 +16,10 @@ func newListCmd(app *App) *cobra.Command {
var exporter cmdutil.Exporter
listCmd := &cobra.Command{
Use: "list",
Short: "List your codespaces",
Args: noArgsConstraint,
Use: "list",
Short: "List your codespaces",
Aliases: []string{"ls"},
Args: noArgsConstraint,
RunE: func(cmd *cobra.Command, args []string) error {
if limit < 1 {
return cmdutil.FlagErrorf("invalid limit: %v", limit)
@ -42,6 +43,14 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er
return fmt.Errorf("error getting codespaces: %w", err)
}
hasNonProdVSCSTarget := false
for _, apiCodespace := range codespaces {
if apiCodespace.VSCSTarget != "" && apiCodespace.VSCSTarget != api.VSCSTargetProduction {
hasNonProdVSCSTarget = true
break
}
}
if err := a.io.StartPager(); err != nil {
a.errLogger.Printf("error starting pager: %v", err)
}
@ -58,6 +67,11 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er
tp.AddField("BRANCH", nil, nil)
tp.AddField("STATE", nil, nil)
tp.AddField("CREATED AT", nil, nil)
if hasNonProdVSCSTarget {
tp.AddField("VSCS TARGET", nil, nil)
}
tp.EndRow()
}
@ -73,10 +87,24 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er
stateColor = cs.Green
}
tp.AddField(c.Name, nil, cs.Yellow)
formattedName := formatNameForVSCSTarget(c.Name, c.VSCSTarget)
var nameColor func(string) string
switch c.PendingOperation {
case false:
nameColor = cs.Yellow
case true:
nameColor = cs.Gray
}
tp.AddField(formattedName, nil, nameColor)
tp.AddField(c.Repository.FullName, nil, nil)
tp.AddField(c.branchWithGitStatus(), nil, cs.Cyan)
tp.AddField(c.State, nil, stateColor)
if c.PendingOperation {
tp.AddField(c.PendingOperationDisabledReason, nil, nameColor)
} else {
tp.AddField(c.State, nil, stateColor)
}
if tp.IsTTY() {
ct, err := time.Parse(time.RFC3339, c.CreatedAt)
@ -87,8 +115,25 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er
} else {
tp.AddField(c.CreatedAt, nil, nil)
}
if hasNonProdVSCSTarget {
tp.AddField(c.VSCSTarget, nil, nil)
}
tp.EndRow()
}
return tp.Render()
}
func formatNameForVSCSTarget(name, vscsTarget string) string {
if vscsTarget == api.VSCSTargetDevelopment || vscsTarget == api.VSCSTargetLocal {
return fmt.Sprintf("%s 🚧", name)
}
if vscsTarget == api.VSCSTargetPPE {
return fmt.Sprintf("%s ✨", name)
}
return name
}

View file

@ -38,7 +38,7 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
if err != nil {
return fmt.Errorf("get or choose codespace: %w", err)
return err
}
authkeys := make(chan error, 1)

View file

@ -0,0 +1,47 @@
package codespace
import (
"context"
"testing"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/pkg/iostreams"
)
func TestPendingOperationDisallowsLogs(t *testing.T) {
app := testingLogsApp()
if err := app.Logs(context.Background(), "disabledCodespace", false); err != nil {
if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" {
t.Errorf("expected pending operation error, but got: %v", err)
}
} else {
t.Error("expected pending operation error, but got nothing")
}
}
func testingLogsApp() *App {
user := &api.User{Login: "monalisa"}
disabledCodespace := &api.Codespace{
Name: "disabledCodespace",
PendingOperation: true,
PendingOperationDisabledReason: "Some pending operation",
}
apiMock := &apiClientMock{
GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) {
if name == "disabledCodespace" {
return disabledCodespace, nil
}
return nil, nil
},
GetUserFunc: func(_ context.Context) (*api.User, error) {
return user, nil
},
AuthorizedKeysFunc: func(_ context.Context, _ string) ([]byte, error) {
return []byte{}, nil
},
}
io, _, _, _ := iostreams.Test()
return NewApp(io, nil, apiMock, nil)
}

View file

@ -25,6 +25,9 @@ import (
// DeleteCodespaceFunc: func(ctx context.Context, name string) error {
// panic("mock out the DeleteCodespace method")
// },
// EditCodespaceFunc: func(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error) {
// panic("mock out the EditCodespace method")
// },
// GetCodespaceFunc: func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
// panic("mock out the GetCodespace method")
// },
@ -71,6 +74,9 @@ type apiClientMock struct {
// DeleteCodespaceFunc mocks the DeleteCodespace method.
DeleteCodespaceFunc func(ctx context.Context, name string) error
// EditCodespaceFunc mocks the EditCodespace method.
EditCodespaceFunc func(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error)
// GetCodespaceFunc mocks the GetCodespace method.
GetCodespaceFunc func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
@ -124,6 +130,15 @@ type apiClientMock struct {
// Name is the name argument value.
Name string
}
// EditCodespace holds details about calls to the EditCodespace method.
EditCodespace []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// CodespaceName is the codespaceName argument value.
CodespaceName string
// Params is the params argument value.
Params *api.EditCodespaceParams
}
// GetCodespace holds details about calls to the GetCodespace method.
GetCodespace []struct {
// Ctx is the ctx argument value.
@ -204,6 +219,7 @@ type apiClientMock struct {
lockAuthorizedKeys sync.RWMutex
lockCreateCodespace sync.RWMutex
lockDeleteCodespace sync.RWMutex
lockEditCodespace sync.RWMutex
lockGetCodespace sync.RWMutex
lockGetCodespaceRegionLocation sync.RWMutex
lockGetCodespaceRepoSuggestions sync.RWMutex
@ -321,6 +337,45 @@ func (mock *apiClientMock) DeleteCodespaceCalls() []struct {
return calls
}
// EditCodespace calls EditCodespaceFunc.
func (mock *apiClientMock) EditCodespace(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error) {
if mock.EditCodespaceFunc == nil {
panic("apiClientMock.EditCodespaceFunc: method is nil but apiClient.EditCodespace was just called")
}
callInfo := struct {
Ctx context.Context
CodespaceName string
Params *api.EditCodespaceParams
}{
Ctx: ctx,
CodespaceName: codespaceName,
Params: params,
}
mock.lockEditCodespace.Lock()
mock.calls.EditCodespace = append(mock.calls.EditCodespace, callInfo)
mock.lockEditCodespace.Unlock()
return mock.EditCodespaceFunc(ctx, codespaceName, params)
}
// EditCodespaceCalls gets all the calls that were made to EditCodespace.
// Check the length with:
// len(mockedapiClient.EditCodespaceCalls())
func (mock *apiClientMock) EditCodespaceCalls() []struct {
Ctx context.Context
CodespaceName string
Params *api.EditCodespaceParams
} {
var calls []struct {
Ctx context.Context
CodespaceName string
Params *api.EditCodespaceParams
}
mock.lockEditCodespace.RLock()
calls = mock.calls.EditCodespace
mock.lockEditCodespace.RUnlock()
return calls
}
// GetCodespace calls GetCodespaceFunc.
func (mock *apiClientMock) GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
if mock.GetCodespaceFunc == nil {

View file

@ -50,11 +50,7 @@ func newPortsCmd(app *App) *cobra.Command {
func (a *App) ListPorts(ctx context.Context, codespaceName string, exporter cmdutil.Exporter) (err error) {
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
if err != nil {
// TODO(josebalius): remove special handling of this error here and it other places
if err == errNoCodespaces {
return err
}
return fmt.Errorf("error choosing codespace: %w", err)
return err
}
devContainerCh := getDevContainer(ctx, a.apiClient, codespace)
@ -263,10 +259,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
if err != nil {
if err == errNoCodespaces {
return err
}
return fmt.Errorf("error getting codespace: %w", err)
return err
}
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace)
@ -371,10 +364,7 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st
codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName)
if err != nil {
if err == errNoCodespaces {
return err
}
return fmt.Errorf("error getting codespace: %w", err)
return err
}
session, err := codespaces.ConnectToLiveshare(ctx, a, noopLogger(), a.apiClient, codespace)

View file

@ -192,6 +192,7 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev
}
io, _, _, _ := iostreams.Test()
a := &App{
io: io,
apiClient: mockApi,
@ -204,3 +205,66 @@ func runUpdateVisibilityTest(t *testing.T, portVisibilities []portVisibility, ev
return a.UpdatePortVisibility(ctx, "codespace-name", portArgs)
}
func TestPendingOperationDisallowsListPorts(t *testing.T) {
app := testingPortsApp()
if err := app.ListPorts(context.Background(), "disabledCodespace", nil); err != nil {
if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" {
t.Errorf("expected pending operation error, but got: %v", err)
}
} else {
t.Error("expected pending operation error, but got nothing")
}
}
func TestPendingOperationDisallowsUpdatePortVisability(t *testing.T) {
app := testingPortsApp()
if err := app.UpdatePortVisibility(context.Background(), "disabledCodespace", nil); err != nil {
if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" {
t.Errorf("expected pending operation error, but got: %v", err)
}
} else {
t.Error("expected pending operation error, but got nothing")
}
}
func TestPendingOperationDisallowsForwardPorts(t *testing.T) {
app := testingPortsApp()
if err := app.ForwardPorts(context.Background(), "disabledCodespace", nil); err != nil {
if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" {
t.Errorf("expected pending operation error, but got: %v", err)
}
} else {
t.Error("expected pending operation error, but got nothing")
}
}
func testingPortsApp() *App {
user := &api.User{Login: "monalisa"}
disabledCodespace := &api.Codespace{
Name: "disabledCodespace",
PendingOperation: true,
PendingOperationDisabledReason: "Some pending operation",
}
apiMock := &apiClientMock{
GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) {
if name == "disabledCodespace" {
return disabledCodespace, nil
}
return nil, nil
},
GetUserFunc: func(_ context.Context) (*api.User, error) {
return user, nil
},
AuthorizedKeysFunc: func(_ context.Context, _ string) ([]byte, error) {
return []byte{}, nil
},
}
io, _, _, _ := iostreams.Test()
return NewApp(io, nil, apiMock, nil)
}

View file

@ -12,6 +12,7 @@ func NewRootCmd(app *App) *cobra.Command {
root.AddCommand(newCodeCmd(app))
root.AddCommand(newCreateCmd(app))
root.AddCommand(newEditCmd(app))
root.AddCommand(newDeleteCmd(app))
root.AddCommand(newListCmd(app))
root.AddCommand(newLogsCmd(app))

View file

@ -125,7 +125,7 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
codespace, err := getOrChooseCodespace(ctx, a.apiClient, opts.codespace)
if err != nil {
return fmt.Errorf("get or choose codespace: %w", err)
return err
}
liveshareLogger := noopLogger()
@ -379,6 +379,7 @@ func newCpCmd(app *App) *cobra.Command {
cpCmd.Flags().BoolVarP(&opts.recursive, "recursive", "r", false, "Recursively copy directories")
cpCmd.Flags().BoolVarP(&opts.expand, "expand", "e", false, "Expand remote file names on remote shell")
cpCmd.Flags().StringVarP(&opts.codespace, "codespace", "c", "", "Name of the codespace")
cpCmd.Flags().StringVarP(&opts.profile, "profile", "p", "", "Name of the SSH profile to use")
return cpCmd
}

View file

@ -0,0 +1,47 @@
package codespace
import (
"context"
"testing"
"github.com/cli/cli/v2/internal/codespaces/api"
"github.com/cli/cli/v2/pkg/iostreams"
)
func TestPendingOperationDisallowsSSH(t *testing.T) {
app := testingSSHApp()
if err := app.SSH(context.Background(), []string{}, sshOptions{codespace: "disabledCodespace"}); err != nil {
if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" {
t.Errorf("expected pending operation error, but got: %v", err)
}
} else {
t.Error("expected pending operation error, but got nothing")
}
}
func testingSSHApp() *App {
user := &api.User{Login: "monalisa"}
disabledCodespace := &api.Codespace{
Name: "disabledCodespace",
PendingOperation: true,
PendingOperationDisabledReason: "Some pending operation",
}
apiMock := &apiClientMock{
GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) {
if name == "disabledCodespace" {
return disabledCodespace, nil
}
return nil, nil
},
GetUserFunc: func(_ context.Context) (*api.User, error) {
return user, nil
},
AuthorizedKeysFunc: func(_ context.Context, _ string) ([]byte, error) {
return []byte{}, nil
},
}
io, _, _, _ := iostreams.Test()
return NewApp(io, nil, apiMock, nil)
}

View file

@ -92,8 +92,7 @@ func NewCmdCompletion(io *iostreams.IOStreams) *cobra.Command {
}
cmdutil.DisableAuthCheck(cmd)
cmd.Flags().StringVarP(&shellType, "shell", "s", "", "Shell type: {bash|zsh|fish|powershell}")
cmdutil.StringEnumFlag(cmd, &shellType, "shell", "s", "", []string{"bash", "zsh", "fish", "powershell"}, "Shell type")
return cmd
}

View file

@ -39,7 +39,7 @@ func TestNewCmdCompletion(t *testing.T) {
{
name: "unsupported shell",
args: "completion -s csh",
wantErr: "unsupported shell type \"csh\"",
wantErr: "invalid argument \"csh\" for \"-s, --shell\" flag: valid values are {bash|zsh|fish|powershell}",
},
}
for _, tt := range tests {

View file

@ -23,9 +23,10 @@ func NewCmdConfigList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.
}
cmd := &cobra.Command{
Use: "list",
Short: "Print a list of configuration keys and values",
Args: cobra.ExactArgs(0),
Use: "list",
Short: "Print a list of configuration keys and values",
Aliases: []string{"ls"},
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
if runF != nil {
return runF(opts)

View file

@ -40,9 +40,10 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
extCmd.AddCommand(
&cobra.Command{
Use: "list",
Short: "List installed extension commands",
Args: cobra.NoArgs,
Use: "list",
Short: "List installed extension commands",
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cmds := m.List(true)
if len(cmds) == 0 {
@ -60,6 +61,11 @@ func NewCmdExtension(f *cmdutil.Factory) *cobra.Command {
t.AddField(fmt.Sprintf("gh %s", c.Name()), nil, nil)
t.AddField(repo, nil, nil)
version := c.CurrentVersion()
if !c.IsBinary() && len(version) > 8 {
version = version[:8]
}
t.AddField(version, nil, nil)
var updateAvailable string
if c.UpdateAvailable() {
updateAvailable = "Upgrade available"

View file

@ -299,7 +299,7 @@ func TestNewCmdExtension(t *testing.T) {
assert.Equal(t, 1, len(em.ListCalls()))
}
},
wantStdout: "gh test\tcli/gh-test\t\ngh test2\tcli/gh-test2\tUpgrade available\n",
wantStdout: "gh test\tcli/gh-test\t1\t\ngh test2\tcli/gh-test2\t1\tUpgrade available\n",
},
{
name: "create extension interactive",

View file

@ -39,6 +39,10 @@ func (e *Extension) IsLocal() bool {
return e.isLocal
}
func (e *Extension) CurrentVersion() string {
return e.currentVersion
}
func (e *Extension) UpdateAvailable() bool {
if e.isLocal ||
e.currentVersion == "" ||

View file

@ -35,9 +35,10 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
var flagSecret bool
cmd := &cobra.Command{
Use: "list",
Short: "List your gists",
Args: cobra.NoArgs,
Use: "list",
Short: "List your gists",
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if opts.Limit < 1 {
return cmdutil.FlagErrorf("invalid limit: %v", opts.Limit)

View file

@ -27,9 +27,10 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
}
cmd := &cobra.Command{
Use: "list",
Short: "Lists GPG keys in your GitHub account",
Args: cobra.ExactArgs(0),
Use: "list",
Short: "Lists GPG keys in your GitHub account",
Aliases: []string{"ls"},
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
if runF != nil {
return runF(opts)

View file

@ -23,9 +23,15 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e
cmd := &cobra.Command{
Use: "comment {<number> | <url>}",
Short: "Create a new issue comment",
Short: "Add a comment to an issue",
Long: heredoc.Doc(`
Add a comment to a GitHub issue.
Without the body text supplied through flags, the command will interactively
prompt for the comment text.
`),
Example: heredoc.Doc(`
$ gh issue comment 22 --body "I was able to reproduce this issue, lets fix it."
$ gh issue comment 12 --body "Hi from GitHub CLI"
`),
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
@ -54,10 +60,10 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e
},
}
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "The comment body `text`")
cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)")
cmd.Flags().BoolP("editor", "e", false, "Add body using editor")
cmd.Flags().BoolP("web", "w", false, "Add body in browser")
cmd.Flags().BoolP("editor", "e", false, "Skip prompts and open the text editor to write the body in")
cmd.Flags().BoolP("web", "w", false, "Open the web browser to write the comment")
return cmd
}

View file

@ -57,16 +57,22 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
cmd := &cobra.Command{
Use: "list",
Short: "List and filter issues in this repository",
Short: "List issues in a repository",
Long: heredoc.Doc(`
List issues in a GitHub repository.
The search query syntax is documented here:
<https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests>
`),
Example: heredoc.Doc(`
$ gh issue list -l "bug" -l "help wanted"
$ gh issue list -A monalisa
$ gh issue list -a "@me"
$ gh issue list --web
$ gh issue list --label "bug" --label "help wanted"
$ gh issue list --author monalisa
$ gh issue list --assignee "@me"
$ gh issue list --milestone "The big 1.0"
$ gh issue list --search "error no:assignee sort:created-asc"
`),
Args: cmdutil.NoArgsQuoteReminder,
Aliases: []string{"ls"},
Args: cmdutil.NoArgsQuoteReminder,
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo
@ -82,17 +88,14 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
},
}
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the browser to list the issue(s)")
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List issues in the web browser")
cmd.Flags().StringVarP(&opts.Assignee, "assignee", "a", "", "Filter by assignee")
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by labels")
cmd.Flags().StringVarP(&opts.State, "state", "s", "open", "Filter by state: {open|closed|all}")
_ = cmd.RegisterFlagCompletionFunc("state", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"open", "closed", "all"}, cobra.ShellCompDirectiveNoFileComp
})
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by label")
cmdutil.StringEnumFlag(cmd, &opts.State, "state", "s", "open", []string{"open", "closed", "all"}, "Filter by state")
cmd.Flags().IntVarP(&opts.LimitResults, "limit", "L", 30, "Maximum number of issues to fetch")
cmd.Flags().StringVarP(&opts.Author, "author", "A", "", "Filter by author")
cmd.Flags().StringVar(&opts.Mention, "mention", "", "Filter by mention")
cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Filter by milestone `number` or `title`")
cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Filter by milestone number or title")
cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search issues with `query`")
cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.IssueFields)

View file

@ -134,7 +134,7 @@ func checksRun(opts *ChecksOptions) error {
for {
findOptions := shared.FindOptions{
Selector: opts.SelectorArg,
Fields: []string{"number", "baseRefName", "statusCheckRollup"},
Fields: []string{"number", "headRefName", "statusCheckRollup"},
}
pr, _, err := opts.Finder.Find(findOptions)
if err != nil {

View file

@ -90,34 +90,36 @@ func closeRun(opts *CloseOptions) error {
if opts.DeleteBranch {
branchSwitchString := ""
apiClient := api.NewClientFromHTTP(httpClient)
localBranchExists := git.HasLocalBranch(pr.HeadRefName)
if opts.DeleteLocalBranch {
currentBranch, err := opts.Branch()
if err != nil {
return err
}
var branchToSwitchTo string
if currentBranch == pr.HeadRefName {
branchToSwitchTo, err = api.RepoDefaultBranch(apiClient, baseRepo)
if err != nil {
return err
}
err = git.CheckoutBranch(branchToSwitchTo)
if err != nil {
return err
}
}
localBranchExists := git.HasLocalBranch(pr.HeadRefName)
if localBranchExists {
currentBranch, err := opts.Branch()
if err != nil {
return err
}
var branchToSwitchTo string
if currentBranch == pr.HeadRefName {
branchToSwitchTo, err = api.RepoDefaultBranch(apiClient, baseRepo)
if err != nil {
return err
}
err = git.CheckoutBranch(branchToSwitchTo)
if err != nil {
return err
}
}
if err := git.DeleteLocalBranch(pr.HeadRefName); err != nil {
return fmt.Errorf("failed to delete local branch %s: %w", cs.Cyan(pr.HeadRefName), err)
}
}
if branchToSwitchTo != "" {
branchSwitchString = fmt.Sprintf(" and switched to branch %s", cs.Cyan(branchToSwitchTo))
if branchToSwitchTo != "" {
branchSwitchString = fmt.Sprintf(" and switched to branch %s", cs.Cyan(branchToSwitchTo))
}
} else {
fmt.Fprintf(opts.IO.ErrOut, "%s Skipped deleting the local branch since current directory is not a git repository \n", cs.WarningIcon())
}
}

View file

@ -230,3 +230,37 @@ func TestPrClose_deleteBranch_sameBranch(t *testing.T) {
Deleted branch trunk and switched to branch main
`), output.Stderr())
}
func TestPrClose_deleteBranch_notInGitRepo(t *testing.T) {
http := &httpmock.Registry{}
defer http.Verify(t)
baseRepo, pr := stubPR("OWNER/REPO:main", "OWNER/REPO:trunk")
pr.Title = "The title of the PR"
shared.RunCommandFinder("96", pr, baseRepo)
http.Register(
httpmock.GraphQL(`mutation PullRequestClose\b`),
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
func(inputs map[string]interface{}) {
assert.Equal(t, inputs["pullRequestId"], "THE-ID")
}),
)
http.Register(
httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/trunk"),
httpmock.StringResponse(`{}`))
cs, cmdTeardown := run.Stub()
defer cmdTeardown(t)
cs.Register(`git rev-parse --verify refs/heads/trunk`, 128, "could not determine current branch: fatal: not a git repository (or any of the parent directories): .git")
output, err := runCommand(http, true, `96 --delete-branch`)
assert.NoError(t, err)
assert.Equal(t, "", output.String())
assert.Equal(t, heredoc.Doc(`
Closed pull request #96 (The title of the PR)
! Skipped deleting the local branch since current directory is not a git repository
Deleted branch trunk
`), output.Stderr())
}

View file

@ -22,17 +22,15 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err
cmd := &cobra.Command{
Use: "comment [<number> | <url> | <branch>]",
Short: "Create a new pr comment",
Short: "Add a comment to a pull request",
Long: heredoc.Doc(`
Create a new pr comment.
Add a comment to a GitHub pull request.
Without an argument, the pull request that belongs to the current branch
is selected.
With '--web', comment on the pull request in a web browser instead.
Without the body text supplied through flags, the command will interactively
prompt for the comment text.
`),
Example: heredoc.Doc(`
$ gh pr comment 22 --body "This looks great, lets get it deployed."
$ gh pr comment 13 --body "Hi from GitHub CLI"
`),
Args: cobra.MaximumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
@ -68,10 +66,10 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err
},
}
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "The comment body `text`")
cmd.Flags().StringVarP(&bodyFile, "body-file", "F", "", "Read body text from `file` (use \"-\" to read from standard input)")
cmd.Flags().BoolP("editor", "e", false, "Add body using editor")
cmd.Flags().BoolP("web", "w", false, "Add body in browser")
cmd.Flags().BoolP("editor", "e", false, "Skip prompts and open the text editor to write the body in")
cmd.Flags().BoolP("web", "w", false, "Open the web browser to write the comment")
return cmd
}

View file

@ -66,7 +66,7 @@ func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Comman
case "never":
opts.UseColor = false
default:
return cmdutil.FlagErrorf("the value for `--color` must be one of \"auto\", \"always\", or \"never\"")
return fmt.Errorf("unsupported color %q", colorFlag)
}
if runF != nil {
@ -76,7 +76,7 @@ func NewCmdDiff(f *cmdutil.Factory, runF func(*DiffOptions) error) *cobra.Comman
},
}
cmd.Flags().StringVar(&colorFlag, "color", "auto", "Use color in diff output: {always|never|auto}")
cmdutil.StringEnumFlag(cmd, &colorFlag, "color", "", "auto", []string{"always", "never", "auto"}, "Use color in diff output")
cmd.Flags().BoolVar(&opts.Patch, "patch", false, "Display diff in patch format")
return cmd

View file

@ -83,7 +83,7 @@ func Test_NewCmdDiff(t *testing.T) {
name: "invalid --color argument",
args: "--color doublerainbow",
isTTY: true,
wantErr: "the value for `--color` must be one of \"auto\", \"always\", or \"never\"",
wantErr: "invalid argument \"doublerainbow\" for \"--color\" flag: valid values are {always|never|auto}",
},
}
for _, tt := range tests {

View file

@ -36,6 +36,7 @@ type ListOptions struct {
HeadBranch string
Labels []string
Author string
AppAuthor string
Assignee string
Search string
Draft string
@ -52,24 +53,28 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
cmd := &cobra.Command{
Use: "list",
Short: "List and filter pull requests in this repository",
Short: "List pull requests in a repository",
Long: heredoc.Doc(`
List pull requests in a GitHub repository.
The search query syntax is documented here:
<https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests>
`),
Example: heredoc.Doc(`
List PRs authored by you
$ gh pr list --author "@me"
List PRs assigned to you
$ gh pr list --assignee "@me"
List PRs by label, combining multiple labels with AND
List only PRs with all of the given labels
$ gh pr list --label bug --label "priority 1"
List PRs using search syntax
Filter PRs using search syntax
$ gh pr list --search "status:success review:required"
Open the list of PRs in a web browser
$ gh pr list --web
Find a PR that introduced a given commit
$ gh pr list --search "<SHA>" --state merged
`),
Args: cmdutil.NoArgsQuoteReminder,
Aliases: []string{"ls"},
Args: cmdutil.NoArgsQuoteReminder,
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo
@ -82,6 +87,14 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
opts.Draft = strconv.FormatBool(draft)
}
if err := cmdutil.MutuallyExclusive(
"specify only `--author` or `--app`",
opts.Author != "",
opts.AppAuthor != "",
); err != nil {
return err
}
if runF != nil {
return runF(opts)
}
@ -89,16 +102,14 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
},
}
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the browser to list the pull requests")
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List pull requests in the web browser")
cmd.Flags().IntVarP(&opts.LimitResults, "limit", "L", 30, "Maximum number of items to fetch")
cmd.Flags().StringVarP(&opts.State, "state", "s", "open", "Filter by state: {open|closed|merged|all}")
_ = cmd.RegisterFlagCompletionFunc("state", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"open", "closed", "merged", "all"}, cobra.ShellCompDirectiveNoFileComp
})
cmdutil.StringEnumFlag(cmd, &opts.State, "state", "s", "open", []string{"open", "closed", "merged", "all"}, "Filter by state")
cmd.Flags().StringVarP(&opts.BaseBranch, "base", "B", "", "Filter by base branch")
cmd.Flags().StringVarP(&opts.HeadBranch, "head", "H", "", "Filter by head branch")
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by labels")
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by label")
cmd.Flags().StringVarP(&opts.Author, "author", "A", "", "Filter by author")
cmd.Flags().StringVar(&opts.AppAuthor, "app", "", "Filter by GitHub App author")
cmd.Flags().StringVarP(&opts.Assignee, "assignee", "a", "", "Filter by assignee")
cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search pull requests with `query`")
cmd.Flags().BoolVarP(&draft, "draft", "d", false, "Filter by draft state")
@ -163,6 +174,10 @@ func listRun(opts *ListOptions) error {
return opts.Browser.Browse(openURL)
}
if opts.AppAuthor != "" {
filters.Author = fmt.Sprintf("app/%s", opts.AppAuthor)
}
listResult, err := listPullRequests(httpClient, baseRepo, filters, opts.LimitResults)
if err != nil {
return err

View file

@ -229,6 +229,53 @@ func TestPRList_filteringDraft(t *testing.T) {
}
}
func TestPRList_filteringAuthor(t *testing.T) {
tests := []struct {
name string
cli string
expectedQuery string
}{
{
name: "author @me",
cli: `--author "@me"`,
expectedQuery: `repo:OWNER/REPO is:pr is:open author:@me`,
},
{
name: "author user",
cli: `--author "monalisa"`,
expectedQuery: `repo:OWNER/REPO is:pr is:open author:monalisa`,
},
{
name: "app author",
cli: `--author "app/dependabot"`,
expectedQuery: `repo:OWNER/REPO is:pr is:open author:app/dependabot`,
},
{
name: "app author with app option",
cli: `--app "dependabot"`,
expectedQuery: `repo:OWNER/REPO is:pr is:open author:app/dependabot`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)
http.Register(
httpmock.GraphQL(`query PullRequestSearch\b`),
httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) {
assert.Equal(t, test.expectedQuery, params["q"].(string))
}))
_, err := runCommand(http, true, test.cli)
if err != nil {
t.Fatal(err)
}
})
}
}
func TestPRList_withInvalidLimitFlag(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)

View file

@ -11,7 +11,6 @@ import (
"github.com/cli/cli/v2/context"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -322,18 +321,35 @@ func mergeRun(opts *MergeOptions) error {
var branchToSwitchTo string
if currentBranch == pr.HeadRefName {
branchToSwitchTo, err = api.RepoDefaultBranch(apiClient, baseRepo)
branchToSwitchTo = pr.BaseRefName
if branchToSwitchTo == "" {
branchToSwitchTo, err = api.RepoDefaultBranch(apiClient, baseRepo)
if err != nil {
return err
}
}
remotes, err := opts.Remotes()
if err != nil {
return err
}
err = git.CheckoutBranch(branchToSwitchTo)
baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName())
if err != nil {
return err
}
err := pullLatestChanges(opts, baseRepo, branchToSwitchTo)
if err != nil {
if git.HasLocalBranch(branchToSwitchTo) {
if err := git.CheckoutBranch(branchToSwitchTo); err != nil {
return err
}
} else {
if err := git.CheckoutNewBranch(baseRemote.Name, branchToSwitchTo); err != nil {
return err
}
}
if err := git.Pull(baseRemote.Name, branchToSwitchTo); err != nil {
fmt.Fprintf(opts.IO.ErrOut, "%s warning: not possible to fast-forward to: %q\n", cs.WarningIcon(), branchToSwitchTo)
}
}
@ -364,25 +380,6 @@ func mergeRun(opts *MergeOptions) error {
return nil
}
func pullLatestChanges(opts *MergeOptions, repo ghrepo.Interface, branch string) error {
remotes, err := opts.Remotes()
if err != nil {
return err
}
baseRemote, err := remotes.FindByRepo(repo.RepoOwner(), repo.RepoName())
if err != nil {
return err
}
err = git.Pull(baseRemote.Name, branch)
if err != nil {
return err
}
return nil
}
func mergeMethodSurvey(baseRepo *api.Repository) (PullRequestMergeMethod, error) {
type mergeOption struct {
title string

View file

@ -480,6 +480,7 @@ func TestPrMerge_deleteBranch(t *testing.T) {
cs, cmdTeardown := run.Stub()
defer cmdTeardown(t)
cs.Register(`git rev-parse --verify refs/heads/master`, 0, "")
cs.Register(`git checkout master`, 0, "")
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
cs.Register(`git branch -D blueberries`, 0, "")
@ -497,6 +498,106 @@ func TestPrMerge_deleteBranch(t *testing.T) {
`), output.Stderr())
}
func TestPrMerge_deleteBranch_nonDefault(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)
shared.RunCommandFinder(
"",
&api.PullRequest{
ID: "PR_10",
Number: 10,
State: "OPEN",
Title: "Blueberries are a good fruit",
HeadRefName: "blueberries",
MergeStateStatus: "CLEAN",
BaseRefName: "fruit",
},
baseRepo("OWNER", "REPO", "master"),
)
http.Register(
httpmock.GraphQL(`mutation PullRequestMerge\b`),
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
assert.Equal(t, "PR_10", input["pullRequestId"].(string))
assert.Equal(t, "MERGE", input["mergeMethod"].(string))
assert.NotContains(t, input, "commitHeadline")
}))
http.Register(
httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"),
httpmock.StringResponse(`{}`))
cs, cmdTeardown := run.Stub()
defer cmdTeardown(t)
cs.Register(`git rev-parse --verify refs/heads/fruit`, 0, "")
cs.Register(`git checkout fruit`, 0, "")
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
cs.Register(`git branch -D blueberries`, 0, "")
cs.Register(`git pull --ff-only`, 0, "")
output, err := runCommand(http, "blueberries", true, `pr merge --merge --delete-branch`)
if err != nil {
t.Fatalf("Got unexpected error running `pr merge` %s", err)
}
assert.Equal(t, "", output.String())
assert.Equal(t, heredoc.Doc(`
Merged pull request #10 (Blueberries are a good fruit)
Deleted branch blueberries and switched to branch fruit
`), output.Stderr())
}
func TestPrMerge_deleteBranch_checkoutNewBranch(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)
shared.RunCommandFinder(
"",
&api.PullRequest{
ID: "PR_10",
Number: 10,
State: "OPEN",
Title: "Blueberries are a good fruit",
HeadRefName: "blueberries",
MergeStateStatus: "CLEAN",
BaseRefName: "fruit",
},
baseRepo("OWNER", "REPO", "master"),
)
http.Register(
httpmock.GraphQL(`mutation PullRequestMerge\b`),
httpmock.GraphQLMutation(`{}`, func(input map[string]interface{}) {
assert.Equal(t, "PR_10", input["pullRequestId"].(string))
assert.Equal(t, "MERGE", input["mergeMethod"].(string))
assert.NotContains(t, input, "commitHeadline")
}))
http.Register(
httpmock.REST("DELETE", "repos/OWNER/REPO/git/refs/heads/blueberries"),
httpmock.StringResponse(`{}`))
cs, cmdTeardown := run.Stub()
defer cmdTeardown(t)
cs.Register(`git rev-parse --verify refs/heads/fruit`, 1, "")
cs.Register(`git checkout -b fruit --track origin/fruit`, 0, "")
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
cs.Register(`git branch -D blueberries`, 0, "")
cs.Register(`git pull --ff-only`, 0, "")
output, err := runCommand(http, "blueberries", true, `pr merge --merge --delete-branch`)
if err != nil {
t.Fatalf("Got unexpected error running `pr merge` %s", err)
}
assert.Equal(t, "", output.String())
assert.Equal(t, heredoc.Doc(`
Merged pull request #10 (Blueberries are a good fruit)
Deleted branch blueberries and switched to branch fruit
`), output.Stderr())
}
func TestPrMerge_deleteNonCurrentBranch(t *testing.T) {
http := initFakeHTTP()
defer http.Verify(t)
@ -764,6 +865,7 @@ func TestPrMerge_alreadyMerged(t *testing.T) {
cs, cmdTeardown := run.Stub()
defer cmdTeardown(t)
cs.Register(`git rev-parse --verify refs/heads/master`, 0, "")
cs.Register(`git checkout master`, 0, "")
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
cs.Register(`git branch -D blueberries`, 0, "")
@ -906,6 +1008,7 @@ func TestPRMerge_interactiveWithDeleteBranch(t *testing.T) {
cs, cmdTeardown := run.Stub()
defer cmdTeardown(t)
cs.Register(`git rev-parse --verify refs/heads/master`, 0, "")
cs.Register(`git checkout master`, 0, "")
cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "")
cs.Register(`git branch -D blueberries`, 0, "")

View file

@ -65,13 +65,9 @@ func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error {
if inputFlags == 0 {
if !opts.IO.CanPrompt() {
return cmdutil.FlagErrorf("`--body`, `--body-file` or `--web` required when not running interactively")
return cmdutil.FlagErrorf("flags required when not running interactively")
}
opts.Interactive = true
} else if inputFlags == 1 {
if !opts.IO.CanPrompt() && opts.InputType == InputTypeEditor {
return cmdutil.FlagErrorf("`--body`, `--body-file` or `--web` required when not running interactively")
}
} else if inputFlags > 1 {
return cmdutil.FlagErrorf("specify only one of `--body`, `--body-file`, `--editor`, or `--web`")
}

View file

@ -174,6 +174,7 @@ func createRun(opts *CreateOptions) error {
return err
}
var existingTag bool
if opts.TagName == "" {
tags, err := getTags(httpClient, baseRepo, 5)
if err != nil {
@ -198,6 +199,7 @@ func createRun(opts *CreateOptions) error {
return fmt.Errorf("could not prompt: %w", err)
}
if tag != createNewTagOption {
existingTag = true
opts.TagName = tag
}
}
@ -213,6 +215,29 @@ func createRun(opts *CreateOptions) error {
}
}
var tagDescription string
if opts.RepoOverride == "" {
tagDescription, _ = gitTagInfo(opts.TagName)
// If there is a local tag with the same name as specified
// the user may not want to create a new tag on the remote
// as the local one might be annotated or signed.
// If the user specifies the target take that as explict instruction
// to create the tag on the remote pointing to the target regardless
// of local tag status.
// If a remote tag with the same name as specified exists already
// then a new tag will not be created so ignore local tag status.
if tagDescription != "" && !existingTag && opts.Target == "" {
remoteExists, err := remoteTagExists(httpClient, baseRepo, opts.TagName)
if err != nil {
return err
}
if !remoteExists {
return fmt.Errorf("tag %s exists locally but has not been pushed to %s, please push it before continuing or specify the `--target` flag to create a new tag",
opts.TagName, ghrepo.FullName(baseRepo))
}
}
}
if !opts.BodyProvided && opts.IO.CanPrompt() {
editorCommand, err := cmdutil.DetermineEditor(opts.Config)
if err != nil {
@ -220,7 +245,6 @@ func createRun(opts *CreateOptions) error {
}
var generatedNotes *releaseNotes
var tagDescription string
var generatedChangelog string
params := map[string]interface{}{
@ -236,7 +260,6 @@ func createRun(opts *CreateOptions) error {
if opts.RepoOverride == "" {
headRef := opts.TagName
tagDescription, _ = gitTagInfo(opts.TagName)
if tagDescription == "" {
if opts.Target != "" {
// TODO: use the remote-tracking version of the branch ref
@ -366,13 +389,6 @@ func createRun(opts *CreateOptions) error {
}
}
if opts.Draft && len(opts.DiscussionCategory) > 0 {
return fmt.Errorf(
"%s Discussions not supported with draft releases",
opts.IO.ColorScheme().FailureIcon(),
)
}
params := map[string]interface{}{
"tag_name": opts.TagName,
"draft": opts.Draft,
@ -420,7 +436,7 @@ func createRun(opts *CreateOptions) error {
}
if !opts.Draft {
rel, err := publishRelease(httpClient, newRelease.APIURL)
rel, err := publishRelease(httpClient, newRelease.APIURL, opts.DiscussionCategory)
if err != nil {
return err
}

View file

@ -287,7 +287,7 @@ func Test_createRun(t *testing.T) {
name string
isTTY bool
opts CreateOptions
wantParams interface{}
httpStubs func(t *testing.T, reg *httpmock.Registry)
wantErr string
wantStdout string
wantStderr string
@ -302,12 +302,20 @@ func Test_createRun(t *testing.T) {
BodyProvided: true,
Target: "",
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"name": "The Big 1.2",
"body": "* Fixed bugs",
"draft": false,
"prerelease": false,
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"name": "The Big 1.2",
"body": "* Fixed bugs",
"draft": false,
"prerelease": false,
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: ``,
@ -323,13 +331,21 @@ func Test_createRun(t *testing.T) {
Target: "",
DiscussionCategory: "General",
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"name": "The Big 1.2",
"body": "* Fixed bugs",
"draft": false,
"prerelease": false,
"discussion_category_name": "General",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"name": "The Big 1.2",
"body": "* Fixed bugs",
"draft": false,
"prerelease": false,
"discussion_category_name": "General",
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: ``,
@ -344,11 +360,19 @@ func Test_createRun(t *testing.T) {
BodyProvided: true,
Target: "main",
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"draft": false,
"prerelease": false,
"target_commitish": "main",
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": false,
"prerelease": false,
"target_commitish": "main",
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: ``,
@ -364,35 +388,22 @@ func Test_createRun(t *testing.T) {
Draft: true,
Target: "",
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: ``,
},
{
name: "discussion category for draft release",
isTTY: true,
opts: CreateOptions{
TagName: "v1.2.3",
Name: "",
Body: "",
BodyProvided: true,
Draft: true,
Target: "",
DiscussionCategory: "general",
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
"discussion_category_name": "general",
},
wantErr: "X Discussions not supported with draft releases",
wantStdout: "",
},
{
name: "with generate notes",
isTTY: true,
@ -404,11 +415,19 @@ func Test_createRun(t *testing.T) {
BodyProvided: true,
GenerateNotes: true,
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"draft": false,
"prerelease": false,
"generate_release_notes": true,
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": false,
"prerelease": false,
"generate_release_notes": true,
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantErr: "",
@ -433,10 +452,97 @@ func Test_createRun(t *testing.T) {
},
Concurrency: 1,
},
wantParams: map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
}, params)
}))
reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) {
q := req.URL.Query()
assert.Equal(t, "ball.tgz", q.Get("name"))
assert.Equal(t, "", q.Get("label"))
return &http.Response{
StatusCode: 201,
Request: req,
Body: ioutil.NopCloser(bytes.NewBufferString(`{}`)),
Header: map[string][]string{
"Content-Type": {"application/json"},
},
}, nil
})
reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"draft": false,
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n",
wantStderr: ``,
},
{
name: "upload files and create discussion",
isTTY: true,
opts: CreateOptions{
TagName: "v1.2.3",
Name: "",
Body: "",
BodyProvided: true,
Draft: false,
Target: "",
Assets: []*shared.AssetForUpload{
{
Name: "ball.tgz",
Open: func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBufferString(`TARBALL`)), nil
},
},
},
DiscussionCategory: "general",
Concurrency: 1,
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.RESTPayload(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
"prerelease": false,
"discussion_category_name": "general",
}, params)
}))
reg.Register(httpmock.REST("POST", "assets/upload"), func(req *http.Request) (*http.Response, error) {
q := req.URL.Query()
assert.Equal(t, "ball.tgz", q.Get("name"))
assert.Equal(t, "", q.Get("label"))
return &http.Response{
StatusCode: 201,
Request: req,
Body: ioutil.NopCloser(bytes.NewBufferString(`{}`)),
Header: map[string][]string{
"Content-Type": {"application/json"},
},
}, nil
})
reg.Register(httpmock.REST("PATCH", "releases/123"), httpmock.RESTPayload(201, `{
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
}`, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"draft": false,
"discussion_category_name": "general",
}, params)
}))
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final\n",
wantStderr: ``,
@ -450,15 +556,10 @@ func Test_createRun(t *testing.T) {
io.SetStderrTTY(tt.isTTY)
fakeHTTP := &httpmock.Registry{}
fakeHTTP.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`))
fakeHTTP.Register(httpmock.REST("POST", "assets/upload"), httpmock.StatusStringResponse(201, `{}`))
fakeHTTP.Register(httpmock.REST("PATCH", "releases/123"), httpmock.StatusStringResponse(201, `{
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3-final"
}`))
if tt.httpStubs != nil {
tt.httpStubs(t, fakeHTTP)
}
defer fakeHTTP.Verify(t)
tt.opts.IO = io
tt.opts.HttpClient = func() (*http.Client, error) {
@ -476,26 +577,6 @@ func Test_createRun(t *testing.T) {
require.NoError(t, err)
}
bb, err := ioutil.ReadAll(fakeHTTP.Requests[0].Body)
require.NoError(t, err)
var params interface{}
err = json.Unmarshal(bb, &params)
require.NoError(t, err)
assert.Equal(t, tt.wantParams, params)
if len(tt.opts.Assets) > 0 {
q := fakeHTTP.Requests[1].URL.Query()
assert.Equal(t, tt.opts.Assets[0].Name, q.Get("name"))
assert.Equal(t, tt.opts.Assets[0].Label, q.Get("label"))
bb, err := ioutil.ReadAll(fakeHTTP.Requests[2].Body)
require.NoError(t, err)
var updateParams interface{}
err = json.Unmarshal(bb, &updateParams)
require.NoError(t, err)
assert.Equal(t, map[string]interface{}{"draft": false}, updateParams)
}
assert.Equal(t, tt.wantStdout, stdout.String())
assert.Equal(t, tt.wantStderr, stderr.String())
})
@ -673,6 +754,8 @@ func Test_createRun_interactive(t *testing.T) {
rs.Register(`git describe --tags --abbrev=0 v1\.2\.3\^`, 1, "")
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.GraphQL("RepositoryFindRef"),
httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`))
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
httpmock.StatusStringResponse(404, `{}`))
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"),
@ -690,6 +773,57 @@ func Test_createRun_interactive(t *testing.T) {
},
wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
},
{
name: "error when unpublished local tag and target not specified",
opts: &CreateOptions{
TagName: "v1.2.3",
},
runStubs: func(rs *run.CommandStubber) {
rs.Register(`git tag --list`, 0, "tag exists")
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.GraphQL("RepositoryFindRef"),
httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": ""}}}}`))
},
wantErr: "tag v1.2.3 exists locally but has not been pushed to OWNER/REPO, please push it before continuing or specify the `--target` flag to create a new tag",
},
{
name: "create a release when unpublished local tag and target specified",
opts: &CreateOptions{
TagName: "v1.2.3",
Target: "main",
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("Title (optional)").AnswerWith("")
as.StubPrompt("Release notes").
AssertOptions([]string{"Write my own", "Write using generated notes as template", "Write using git tag message as template", "Leave blank"}).
AnswerWith("Leave blank")
as.StubPrompt("Is this a prerelease?").AnswerWith(false)
as.StubPrompt("Submit?").AnswerWith("Publish release")
},
runStubs: func(rs *run.CommandStubber) {
rs.Register(`git tag --list`, 0, "tag exists")
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases/generate-notes"),
httpmock.StatusStringResponse(200, `{
"name": "generated name",
"body": "generated body"
}`))
reg.Register(httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusStringResponse(201, `{
"url": "https://api.github.com/releases/123",
"upload_url": "https://api.github.com/assets/upload",
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`))
},
wantParams: map[string]interface{}{
"draft": false,
"prerelease": false,
"tag_name": "v1.2.3",
"target_commitish": "main",
},
wantOut: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
},
}
for _, tt := range tests {
ios, _, stdout, stderr := iostreams.Test()
@ -739,7 +873,17 @@ func Test_createRun_interactive(t *testing.T) {
}
if tt.wantParams != nil {
bb, err := ioutil.ReadAll(reg.Requests[1].Body)
var r *http.Request
for _, req := range reg.Requests {
if req.URL.Path == "/repos/OWNER/REPO/releases" {
r = req
break
}
}
if r == nil {
t.Fatalf("no http requests for creating a release found")
}
bb, err := ioutil.ReadAll(r.Body)
assert.NoError(t, err)
var params map[string]interface{}
err = json.Unmarshal(bb, &params)

View file

@ -2,6 +2,7 @@ package create
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
@ -13,6 +14,7 @@ import (
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/release/shared"
graphql "github.com/cli/shurcooL-graphql"
)
type tag struct {
@ -26,6 +28,25 @@ type releaseNotes struct {
var notImplementedError = errors.New("not implemented")
func remoteTagExists(httpClient *http.Client, repo ghrepo.Interface, tagName string) (bool, error) {
gql := graphql.NewClient(ghinstance.GraphQLEndpoint(repo.RepoHost()), httpClient)
qualifiedTagName := fmt.Sprintf("refs/tags/%s", tagName)
var query struct {
Repository struct {
Ref struct {
ID string
} `graphql:"ref(qualifiedName: $tagName)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}
variables := map[string]interface{}{
"owner": graphql.String(repo.RepoOwner()),
"name": graphql.String(repo.RepoName()),
"tagName": graphql.String(qualifiedTagName),
}
err := gql.QueryNamed(context.Background(), "RepositoryFindRef", &query, variables)
return query.Repository.Ref.ID != "", err
}
func getTags(httpClient *http.Client, repo ghrepo.Interface, limit int) ([]tag, error) {
path := fmt.Sprintf("repos/%s/%s/tags?per_page=%d", repo.RepoOwner(), repo.RepoName(), limit)
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
@ -134,8 +155,17 @@ func createRelease(httpClient *http.Client, repo ghrepo.Interface, params map[st
return &newRelease, err
}
func publishRelease(httpClient *http.Client, releaseURL string) (*shared.Release, error) {
req, err := http.NewRequest("PATCH", releaseURL, bytes.NewBufferString(`{"draft":false}`))
func publishRelease(httpClient *http.Client, releaseURL string, discussionCategory string) (*shared.Release, error) {
params := map[string]interface{}{"draft": false}
if discussionCategory != "" {
params["discussion_category_name"] = discussionCategory
}
bodyBytes, err := json.Marshal(params)
if err != nil {
return nil, err
}
req, err := http.NewRequest("PATCH", releaseURL, bytes.NewBuffer(bodyBytes))
if err != nil {
return nil, err
}

View file

@ -28,9 +28,10 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
}
cmd := &cobra.Command{
Use: "list",
Short: "List releases in a repository",
Args: cobra.NoArgs,
Use: "list",
Short: "List releases in a repository",
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo

View file

@ -15,7 +15,7 @@ import (
func NewCmdRelease(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "release <command>",
Short: "Manage GitHub releases",
Short: "Manage releases",
Annotations: map[string]string{
"IsCore": "true",
},

View file

@ -26,9 +26,10 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
}
cmd := &cobra.Command{
Use: "list",
Short: "List deploy keys in a GitHub repository",
Args: cobra.ExactArgs(0),
Use: "list",
Short: "List deploy keys in a GitHub repository",
Aliases: []string{"ls"},
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
opts.BaseRepo = f.BaseRepo

View file

@ -8,44 +8,73 @@ import (
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/pkg/set"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)
const (
allowMergeCommits = "Allow Merge Commits"
allowSquashMerge = "Allow Squash Merging"
allowRebaseMerge = "Allow Rebase Merging"
optionAllowForking = "Allow Forking"
optionDefaultBranchName = "Default Branch Name"
optionDescription = "Description"
optionHomePageURL = "Home Page URL"
optionIssues = "Issues"
optionMergeOptions = "Merge Options"
optionProjects = "Projects"
optionTemplateRepo = "Template Repository"
optionTopics = "Topics"
optionVisibility = "Visibility"
optionWikis = "Wikis"
)
type EditOptions struct {
HTTPClient *http.Client
Repository ghrepo.Interface
Edits EditRepositoryInput
AddTopics []string
RemoveTopics []string
HTTPClient *http.Client
Repository ghrepo.Interface
IO *iostreams.IOStreams
Edits EditRepositoryInput
AddTopics []string
RemoveTopics []string
InteractiveMode bool
// Cache of current repo topics to avoid retrieving them
// in multiple flows.
topicsCache []string
}
type EditRepositoryInput struct {
Description *string `json:"description,omitempty"`
Homepage *string `json:"homepage,omitempty"`
Visibility *string `json:"visibility,omitempty"`
EnableIssues *bool `json:"has_issues,omitempty"`
EnableProjects *bool `json:"has_projects,omitempty"`
EnableWiki *bool `json:"has_wiki,omitempty"`
IsTemplate *bool `json:"is_template,omitempty"`
DefaultBranch *string `json:"default_branch,omitempty"`
EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"`
EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"`
EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"`
EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"`
DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"`
AllowForking *bool `json:"allow_forking,omitempty"`
DefaultBranch *string `json:"default_branch,omitempty"`
DeleteBranchOnMerge *bool `json:"delete_branch_on_merge,omitempty"`
Description *string `json:"description,omitempty"`
EnableAutoMerge *bool `json:"allow_auto_merge,omitempty"`
EnableIssues *bool `json:"has_issues,omitempty"`
EnableMergeCommit *bool `json:"allow_merge_commit,omitempty"`
EnableProjects *bool `json:"has_projects,omitempty"`
EnableRebaseMerge *bool `json:"allow_rebase_merge,omitempty"`
EnableSquashMerge *bool `json:"allow_squash_merge,omitempty"`
EnableWiki *bool `json:"has_wiki,omitempty"`
Homepage *string `json:"homepage,omitempty"`
IsTemplate *bool `json:"is_template,omitempty"`
Visibility *string `json:"visibility,omitempty"`
}
func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobra.Command {
opts := &EditOptions{}
opts := &EditOptions{
IO: f.IOStreams,
}
cmd := &cobra.Command{
Use: "edit [<repository>]",
@ -57,12 +86,20 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
- by URL, e.g. "https://github.com/OWNER/REPO"
`),
},
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().NFlag() == 0 {
return cmdutil.FlagErrorf("at least one flag is required")
}
Long: heredoc.Docf(`
Edit repository settings.
To toggle a setting off, use the %[1]s--flag=false%[1]s syntax.
`, "`"),
Args: cobra.MaximumNArgs(1),
Example: heredoc.Doc(`
# enable issues and wiki
gh repo edit --enable-issues --enable-wiki
# disable projects
gh repo edit --enable-projects=false
`),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
var err error
opts.Repository, err = ghrepo.FromFullName(args[0])
@ -83,6 +120,14 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
return err
}
if cmd.Flags().NFlag() == 0 {
opts.InteractiveMode = true
}
if opts.InteractiveMode && !opts.IO.CanPrompt() {
return cmdutil.FlagErrorf("specify properties to edit when not running interactively")
}
if runF != nil {
return runF(opts)
}
@ -112,6 +157,38 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr
func editRun(ctx context.Context, opts *EditOptions) error {
repo := opts.Repository
if opts.InteractiveMode {
apiClient := api.NewClientFromHTTP(opts.HTTPClient)
fieldsToRetrieve := []string{
"autoMergeAllowed",
"defaultBranchRef",
"deleteBranchOnMerge",
"description",
"hasIssuesEnabled",
"hasProjectsEnabled",
"hasWikiEnabled",
"homepageUrl",
"isInOrganization",
"isTemplate",
"mergeCommitAllowed",
"rebaseMergeAllowed",
"repositoryTopics",
"squashMergeAllowed",
"visibility",
}
opts.IO.StartProgressIndicator()
fetchedRepo, err := api.FetchRepository(apiClient, opts.Repository, fieldsToRetrieve)
opts.IO.StopProgressIndicator()
if err != nil {
return err
}
err = interactiveRepoEdit(opts, fetchedRepo)
if err != nil {
return err
}
}
apiPath := fmt.Sprintf("repos/%s/%s", repo.RepoOwner(), repo.RepoName())
body := &bytes.Buffer{}
@ -132,16 +209,19 @@ func editRun(ctx context.Context, opts *EditOptions) error {
if len(opts.AddTopics) > 0 || len(opts.RemoveTopics) > 0 {
g.Go(func() error {
existingTopics, err := getTopics(ctx, opts.HTTPClient, repo)
if err != nil {
return err
// opts.topicsCache gets populated in interactive mode
if !opts.InteractiveMode {
var err error
opts.topicsCache, err = getTopics(ctx, opts.HTTPClient, repo)
if err != nil {
return err
}
}
oldTopics := set.NewStringSet()
oldTopics.AddValues(existingTopics)
oldTopics.AddValues(opts.topicsCache)
newTopics := set.NewStringSet()
newTopics.AddValues(existingTopics)
newTopics.AddValues(opts.topicsCache)
newTopics.AddValues(opts.AddTopics)
newTopics.RemoveValues(opts.RemoveTopics)
@ -152,7 +232,209 @@ func editRun(ctx context.Context, opts *EditOptions) error {
})
}
return g.Wait()
err := g.Wait()
if err != nil {
return err
}
if opts.IO.IsStdoutTTY() {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out,
"%s Edited repository %s\n",
cs.SuccessIcon(),
ghrepo.FullName(repo))
}
return nil
}
func interactiveChoice(r *api.Repository) ([]string, error) {
options := []string{
optionDefaultBranchName,
optionDescription,
optionHomePageURL,
optionIssues,
optionMergeOptions,
optionProjects,
optionTemplateRepo,
optionTopics,
optionVisibility,
optionWikis,
}
if r.IsInOrganization {
options = append(options, optionAllowForking)
}
var answers []string
err := prompt.SurveyAskOne(&survey.MultiSelect{
Message: "What do you want to edit?",
Options: options,
}, &answers, survey.WithPageSize(11))
return answers, err
}
func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
for _, v := range r.RepositoryTopics.Nodes {
opts.topicsCache = append(opts.topicsCache, v.Topic.Name)
}
choices, err := interactiveChoice(r)
if err != nil {
return err
}
for _, c := range choices {
switch c {
case optionDescription:
opts.Edits.Description = &r.Description
err = prompt.SurveyAskOne(&survey.Input{
Message: "Description of the repository",
Default: r.Description,
}, opts.Edits.Description)
if err != nil {
return err
}
case optionHomePageURL:
opts.Edits.Homepage = &r.HomepageURL
err = prompt.SurveyAskOne(&survey.Input{
Message: "Repository home page URL",
Default: r.HomepageURL,
}, opts.Edits.Homepage)
if err != nil {
return err
}
case optionTopics:
var addTopics string
err = prompt.SurveyAskOne(&survey.Input{
Message: "Add topics?(csv format)",
}, &addTopics)
if err != nil {
return err
}
if len(strings.TrimSpace(addTopics)) > 0 {
opts.AddTopics = strings.Split(addTopics, ",")
}
if len(opts.topicsCache) > 0 {
err = prompt.SurveyAskOne(&survey.MultiSelect{
Message: "Remove Topics",
Options: opts.topicsCache,
}, &opts.RemoveTopics)
if err != nil {
return err
}
}
case optionDefaultBranchName:
opts.Edits.DefaultBranch = &r.DefaultBranchRef.Name
err = prompt.SurveyAskOne(&survey.Input{
Message: "Default branch name",
Default: r.DefaultBranchRef.Name,
}, opts.Edits.DefaultBranch)
if err != nil {
return err
}
case optionWikis:
opts.Edits.EnableWiki = &r.HasWikiEnabled
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Enable Wikis?",
Default: r.HasWikiEnabled,
}, opts.Edits.EnableWiki)
if err != nil {
return err
}
case optionIssues:
opts.Edits.EnableIssues = &r.HasIssuesEnabled
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Enable Issues?",
Default: r.HasIssuesEnabled,
}, opts.Edits.EnableIssues)
if err != nil {
return err
}
case optionProjects:
opts.Edits.EnableProjects = &r.HasProjectsEnabled
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Enable Projects?",
Default: r.HasProjectsEnabled,
}, opts.Edits.EnableProjects)
if err != nil {
return err
}
case optionVisibility:
opts.Edits.Visibility = &r.Visibility
err = prompt.SurveyAskOne(&survey.Select{
Message: "Visibility",
Options: []string{"public", "private", "internal"},
Default: strings.ToLower(r.Visibility),
}, opts.Edits.Visibility)
if err != nil {
return err
}
case optionMergeOptions:
var defaultMergeOptions []string
var selectedMergeOptions []string
if r.MergeCommitAllowed {
defaultMergeOptions = append(defaultMergeOptions, allowMergeCommits)
}
if r.SquashMergeAllowed {
defaultMergeOptions = append(defaultMergeOptions, allowSquashMerge)
}
if r.RebaseMergeAllowed {
defaultMergeOptions = append(defaultMergeOptions, allowRebaseMerge)
}
err = prompt.SurveyAskOne(&survey.MultiSelect{
Message: "Allowed merge strategies",
Default: defaultMergeOptions,
Options: []string{allowMergeCommits, allowSquashMerge, allowRebaseMerge},
}, &selectedMergeOptions)
if err != nil {
return err
}
enableMergeCommit := isIncluded(allowMergeCommits, selectedMergeOptions)
opts.Edits.EnableMergeCommit = &enableMergeCommit
enableSquashMerge := isIncluded(allowSquashMerge, selectedMergeOptions)
opts.Edits.EnableSquashMerge = &enableSquashMerge
enableRebaseMerge := isIncluded(allowRebaseMerge, selectedMergeOptions)
opts.Edits.EnableRebaseMerge = &enableRebaseMerge
if !enableMergeCommit && !enableSquashMerge && !enableRebaseMerge {
return fmt.Errorf("you need to allow at least one merge strategy")
}
opts.Edits.EnableAutoMerge = &r.AutoMergeAllowed
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Enable Auto Merge?",
Default: r.AutoMergeAllowed,
}, opts.Edits.EnableAutoMerge)
if err != nil {
return err
}
opts.Edits.DeleteBranchOnMerge = &r.DeleteBranchOnMerge
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Automatically delete head branches after merging?",
Default: r.DeleteBranchOnMerge,
}, opts.Edits.DeleteBranchOnMerge)
if err != nil {
return err
}
case optionTemplateRepo:
opts.Edits.IsTemplate = &r.IsTemplate
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Convert into a template repository?",
Default: r.IsTemplate,
}, opts.Edits.IsTemplate)
if err != nil {
return err
}
case optionAllowForking:
opts.Edits.AllowForking = &r.ForkingAllowed
err = prompt.SurveyAskOne(&survey.Confirm{
Message: "Allow forking (of an organization repository)?",
Default: r.ForkingAllowed,
}, opts.Edits.AllowForking)
if err != nil {
return err
}
}
}
return nil
}
func getTopics(ctx context.Context, httpClient *http.Client, repo ghrepo.Interface) ([]string, error) {
@ -216,3 +498,12 @@ func setTopics(ctx context.Context, httpClient *http.Client, repo ghrepo.Interfa
return nil
}
func isIncluded(value string, opts []string) bool {
for _, opt := range opts {
if strings.EqualFold(opt, value) {
return true
}
}
return false
}

View file

@ -7,6 +7,8 @@ import (
"net/http"
"testing"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
@ -23,11 +25,6 @@ func TestNewCmdEdit(t *testing.T) {
wantOpts EditOptions
wantErr string
}{
{
name: "no argument",
args: "",
wantErr: "at least one flag is required",
},
{
name: "change repo description",
args: "--description hello",
@ -135,6 +132,11 @@ func Test_editRun(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
io.SetStdoutTTY(true)
io.SetStdinTTY(true)
io.SetStderrTTY(true)
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)
if tt.httpStubs != nil {
@ -143,6 +145,186 @@ func Test_editRun(t *testing.T) {
opts := &tt.opts
opts.HTTPClient = &http.Client{Transport: httpReg}
opts.IO = io
err := editRun(context.Background(), opts)
if tt.wantsErr == "" {
require.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantsErr)
return
}
})
}
}
func Test_editRun_interactive(t *testing.T) {
tests := []struct {
name string
opts EditOptions
askStubs func(*prompt.AskStubber)
httpStubs func(*testing.T, *httpmock.Registry)
wantsStderr string
wantsErr string
}{
{
name: "updates repo description",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
InteractiveMode: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Description"})
as.StubPrompt("Description of the repository").AnswerWith("awesome repo description")
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{
"data": {
"repository": {
"description": "old description",
"homePageUrl": "https://url.com",
"defaultBranchRef": {
"name": "main"
},
"isInOrganization": false,
"repositoryTopics": {
"nodes": [{
"topic": {
"name": "x"
}
}]
}
}
}
}`))
reg.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO"),
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
assert.Equal(t, "awesome repo description", payload["description"])
}))
},
},
{
name: "updates repo topics",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
InteractiveMode: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Description", "Topics"})
as.StubPrompt("Description of the repository").AnswerWith("awesome repo description")
as.StubPrompt("Add topics?(csv format)").AnswerWith("a,b,c,d")
as.StubPrompt("Remove Topics").AnswerWith([]string{"x"})
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{
"data": {
"repository": {
"description": "old description",
"homePageUrl": "https://url.com",
"defaultBranchRef": {
"name": "main"
},
"isInOrganization": false,
"repositoryTopics": {
"nodes": [{
"topic": {
"name": "x"
}
}]
}
}
}
}`))
reg.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO"),
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
assert.Equal(t, "awesome repo description", payload["description"])
}))
reg.Register(
httpmock.REST("PUT", "repos/OWNER/REPO/topics"),
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
assert.Equal(t, []interface{}{"a", "b", "c", "d"}, payload["names"])
}))
},
},
{
name: "updates repo merge options",
opts: EditOptions{
Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
InteractiveMode: true,
},
askStubs: func(as *prompt.AskStubber) {
as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Merge Options"})
as.StubPrompt("Allowed merge strategies").AnswerWith([]string{allowMergeCommits, allowRebaseMerge})
as.StubPrompt("Enable Auto Merge?").AnswerWith(false)
as.StubPrompt("Automatically delete head branches after merging?").AnswerWith(false)
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query RepositoryInfo\b`),
httpmock.StringResponse(`
{
"data": {
"repository": {
"description": "old description",
"homePageUrl": "https://url.com",
"defaultBranchRef": {
"name": "main"
},
"isInOrganization": false,
"squashMergeAllowed": false,
"rebaseMergeAllowed": true,
"mergeCommitAllowed": true,
"deleteBranchOnMerge": false,
"repositoryTopics": {
"nodes": [{
"topic": {
"name": "x"
}
}]
}
}
}
}`))
reg.Register(
httpmock.REST("PATCH", "repos/OWNER/REPO"),
httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
assert.Equal(t, true, payload["allow_merge_commit"])
assert.Equal(t, false, payload["allow_squash_merge"])
assert.Equal(t, true, payload["allow_rebase_merge"])
}))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
io.SetStdoutTTY(true)
io.SetStdinTTY(true)
io.SetStderrTTY(true)
httpReg := &httpmock.Registry{}
defer httpReg.Verify(t)
if tt.httpStubs != nil {
tt.httpStubs(t, httpReg)
}
opts := &tt.opts
opts.HTTPClient = &http.Client{Transport: httpReg}
opts.IO = io
as := prompt.NewAskStubber(t)
if tt.askStubs != nil {
tt.askStubs(as)
}
err := editRun(context.Background(), opts)
if tt.wantsErr == "" {

View file

@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/context"
"github.com/cli/cli/v2/git"
@ -61,21 +62,23 @@ func NewCmdFork(f *cmdutil.Factory, runF func(*ForkOptions) error) *cobra.Comman
Use: "fork [<repository>] [-- <gitflags>...]",
Args: func(cmd *cobra.Command, args []string) error {
if cmd.ArgsLenAtDash() == 0 && len(args[1:]) > 0 {
return cmdutil.FlagErrorf("repository argument required when passing 'git clone' flags")
return cmdutil.FlagErrorf("repository argument required when passing git clone flags")
}
return nil
},
Short: "Create a fork of a repository",
Long: `Create a fork of a repository.
Long: heredoc.Docf(`
Create a fork of a repository.
With no argument, creates a fork of the current repository. Otherwise, forks
the specified repository.
With no argument, creates a fork of the current repository. Otherwise, forks
the specified repository.
By default, the new fork is set to be your 'origin' remote and any existing
origin remote is renamed to 'upstream'. To alter this behavior, you can set
a name for the new fork's remote with --remote-name.
By default, the new fork is set to be your "origin" remote and any existing
origin remote is renamed to "upstream". To alter this behavior, you can set
a name for the new fork's remote with %[1]s--remote-name%[1]s.
Additional 'git clone' flags can be passed in by listing them after '--'.`,
Additional git clone flags can be passed after %[1]s--%[1]s.
`, "`"),
RunE: func(cmd *cobra.Command, args []string) error {
promptOk := opts.IO.CanPrompt()
if len(args) > 0 {
@ -109,14 +112,14 @@ Additional 'git clone' flags can be passed in by listing them after '--'.`,
if err == pflag.ErrHelp {
return err
}
return cmdutil.FlagErrorf("%w\nSeparate git clone flags with '--'.", err)
return cmdutil.FlagErrorf("%w\nSeparate git clone flags with `--`.", err)
})
cmd.Flags().BoolVar(&opts.Clone, "clone", false, "Clone the fork {true|false}")
cmd.Flags().BoolVar(&opts.Remote, "remote", false, "Add remote for fork {true|false}")
cmd.Flags().StringVar(&opts.RemoteName, "remote-name", defaultRemoteName, "Specify a name for a fork's new remote.")
cmd.Flags().BoolVar(&opts.Clone, "clone", false, "Clone the fork")
cmd.Flags().BoolVar(&opts.Remote, "remote", false, "Add a git remote for the fork")
cmd.Flags().StringVar(&opts.RemoteName, "remote-name", defaultRemoteName, "Specify the name for the new remote")
cmd.Flags().StringVar(&opts.Organization, "org", "", "Create the fork in an organization")
cmd.Flags().StringVar(&opts.ForkName, "fork-name", "", "Specify a name for the forked repo")
cmd.Flags().StringVar(&opts.ForkName, "fork-name", "", "Rename the forked repository")
return cmd
}

View file

@ -45,7 +45,7 @@ func TestNewCmdFork(t *testing.T) {
name: "git args without repo",
cli: "-- --foo bar",
wantErr: true,
errMsg: "repository argument required when passing 'git clone' flags",
errMsg: "repository argument required when passing git clone flags",
},
{
name: "repo",
@ -130,7 +130,7 @@ func TestNewCmdFork(t *testing.T) {
name: "git flags in wrong place",
cli: "--depth 1 OWNER/REPO",
wantErr: true,
errMsg: "unknown flag: --depth\nSeparate git clone flags with '--'.",
errMsg: "unknown flag: --depth\nSeparate git clone flags with `--`.",
},
{
name: "with fork name",

View file

@ -22,7 +22,7 @@ import (
func NewCmdRepo(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "repo <command>",
Short: "Create, clone, fork, and view repositories",
Short: "Manage repositories",
Long: `Work with GitHub repositories.`,
Example: heredoc.Doc(`
$ gh repo create

View file

@ -3,6 +3,7 @@ package root
import (
"bytes"
"fmt"
"sort"
"strings"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -88,6 +89,7 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
return
}
namePadding := 12
coreCommands := []string{}
actionsCommands := []string{}
additionalCommands := []string{}
@ -99,7 +101,7 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
continue
}
s := rpad(c.Name()+":", c.NamePadding()) + c.Short
s := rpad(c.Name()+":", namePadding) + c.Short
if _, ok := c.Annotations["IsCore"]; ok {
coreCommands = append(coreCommands, s)
} else if _, ok := c.Annotations["IsActions"]; ok {
@ -145,6 +147,16 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
}
if isRootCmd(command) {
var helpTopics []string
if c := findCommand(command, "actions"); c != nil {
helpTopics = append(helpTopics, rpad(c.Name()+":", namePadding)+c.Short)
}
for topic, params := range HelpTopics {
helpTopics = append(helpTopics, rpad(topic+":", namePadding)+params["short"])
}
sort.Strings(helpTopics)
helpEntries = append(helpEntries, helpEntry{"HELP TOPICS", strings.Join(helpTopics, "\n")})
if exts := f.ExtensionManager.List(false); len(exts) > 0 {
var names []string
for _, ext := range exts {
@ -192,6 +204,15 @@ Read the manual at https://cli.github.com/manual`})
}
}
func findCommand(cmd *cobra.Command, name string) *cobra.Command {
for _, c := range cmd.Commands() {
if c.Name() == name {
return c
}
}
return nil
}
// rpad adds padding to the right of a string.
func rpad(s string, padding int) string {
template := fmt.Sprintf("%%-%ds ", padding)

View file

@ -25,6 +25,7 @@ import (
repoCmd "github.com/cli/cli/v2/pkg/cmd/repo"
creditsCmd "github.com/cli/cli/v2/pkg/cmd/repo/credits"
runCmd "github.com/cli/cli/v2/pkg/cmd/run"
searchCmd "github.com/cli/cli/v2/pkg/cmd/search"
secretCmd "github.com/cli/cli/v2/pkg/cmd/secret"
sshKeyCmd "github.com/cli/cli/v2/pkg/cmd/ssh-key"
versionCmd "github.com/cli/cli/v2/pkg/cmd/version"
@ -50,9 +51,6 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
"help:feedback": heredoc.Doc(`
Open an issue using 'gh issue create -R github.com/cli/cli'
`),
"help:environment": heredoc.Doc(`
See 'gh help environment' for the list of supported environment variables.
`),
},
}
@ -82,6 +80,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
cmd.AddCommand(gpgKeyCmd.NewCmdGPGKey(f))
cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams))
cmd.AddCommand(extensionCmd.NewCmdExtension(f))
cmd.AddCommand(searchCmd.NewCmdSearch(f))
cmd.AddCommand(secretCmd.NewCmdSecret(f))
cmd.AddCommand(sshKeyCmd.NewCmdSSHKey(f))
cmd.AddCommand(newCodespaceCmd(f))
@ -142,6 +141,7 @@ func newCodespaceCmd(f *cmdutil.Factory) *cobra.Command {
vscsURL,
&lazyLoadedHTTPClient{factory: f},
),
f.Browser,
)
cmd := codespaceCmd.NewRootCmd(app)
cmd.Use = "codespace"

View file

@ -40,9 +40,10 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
}
cmd := &cobra.Command{
Use: "list",
Short: "List recent workflow runs",
Args: cobra.NoArgs,
Use: "list",
Short: "List recent workflow runs",
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo

View file

@ -108,24 +108,6 @@ func TestNewCmdList(t *testing.T) {
}
func TestListRun(t *testing.T) {
// helper to match mocked requests by their query params along with method and path
queryMatcher := func(method string, path string, query url.Values) httpmock.Matcher {
return func(req *http.Request) bool {
if !httpmock.REST(method, path)(req) {
return false
}
actualQuery := req.URL.Query()
for param := range query {
if !(actualQuery.Get(param) == query.Get(param)) {
return false
}
}
return true
}
}
tests := []struct {
name string
opts *ListOptions
@ -244,7 +226,7 @@ func TestListRun(t *testing.T) {
},
stubs: func(reg *httpmock.Registry) {
reg.Register(
queryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{
httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{
"branch": []string{"the-branch"},
}),
httpmock.JSONResponse(shared.RunsPayload{}),
@ -261,7 +243,7 @@ func TestListRun(t *testing.T) {
},
stubs: func(reg *httpmock.Registry) {
reg.Register(
queryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{
httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{
"actor": []string{"bak1an"},
}),
httpmock.JSONResponse(shared.RunsPayload{}),

View file

@ -18,7 +18,9 @@ type RerunOptions struct {
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
RunID string
RunID string
OnlyFailed bool
JobID string
Prompt bool
}
@ -37,12 +39,18 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo
if len(args) > 0 {
if len(args) == 0 && opts.JobID == "" {
if !opts.IO.CanPrompt() {
return cmdutil.FlagErrorf("`<run-id>` or `--job` required when not running interactively")
} else {
opts.Prompt = true
}
} else if len(args) > 0 {
opts.RunID = args[0]
} else if !opts.IO.CanPrompt() {
return cmdutil.FlagErrorf("run ID required when not running interactively")
} else {
opts.Prompt = true
}
if opts.RunID != "" && opts.JobID != "" {
return cmdutil.FlagErrorf("specify only one of `<run-id>` or `--job`")
}
if runF != nil {
@ -52,6 +60,9 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm
},
}
cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs, including dependencies")
cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "Rerun a specific job from a run, including dependencies")
return cmd
}
@ -67,10 +78,23 @@ func runRerun(opts *RerunOptions) error {
return fmt.Errorf("failed to determine base repo: %w", err)
}
cs := opts.IO.ColorScheme()
runID := opts.RunID
jobID := opts.JobID
var selectedJob *shared.Job
if jobID != "" {
opts.IO.StartProgressIndicator()
selectedJob, err = shared.GetJob(client, repo, jobID)
opts.IO.StopProgressIndicator()
if err != nil {
return fmt.Errorf("failed to get job: %w", err)
}
runID = fmt.Sprintf("%d", selectedJob.RunID)
}
if opts.Prompt {
cs := opts.IO.ColorScheme()
runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool {
if run.Status != shared.Completed {
return false
@ -83,7 +107,7 @@ func runRerun(opts *RerunOptions) error {
return fmt.Errorf("failed to get runs: %w", err)
}
if len(runs) == 0 {
return errors.New("no recent runs have failed; please specify a specific run ID")
return errors.New("no recent runs have failed; please specify a specific `<run-id>`")
}
runID, err = shared.PromptForRun(cs, runs)
if err != nil {
@ -91,30 +115,73 @@ func runRerun(opts *RerunOptions) error {
}
}
opts.IO.StartProgressIndicator()
run, err := shared.GetRun(client, repo, runID)
opts.IO.StopProgressIndicator()
if err != nil {
return fmt.Errorf("failed to get run: %w", err)
}
path := fmt.Sprintf("repos/%s/actions/runs/%d/rerun", ghrepo.FullName(repo), run.ID)
err = client.REST(repo.RepoHost(), "POST", path, nil, nil)
if err != nil {
var httpError api.HTTPError
if errors.As(err, &httpError) && httpError.StatusCode == 403 {
return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken.", run.ID)
if opts.JobID != "" {
err = rerunJob(client, repo, selectedJob)
if err != nil {
return err
}
if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.Out, "%s Requested rerun of job %s on run %s\n",
cs.SuccessIcon(),
cs.Cyanf("%d", selectedJob.ID),
cs.Cyanf("%d", selectedJob.RunID))
}
} else {
opts.IO.StartProgressIndicator()
run, err := shared.GetRun(client, repo, runID)
opts.IO.StopProgressIndicator()
if err != nil {
return fmt.Errorf("failed to get run: %w", err)
}
return fmt.Errorf("failed to rerun: %w", err)
}
if opts.IO.CanPrompt() {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out, "%s Requested rerun of run %s\n",
cs.SuccessIcon(),
cs.Cyanf("%d", run.ID))
err = rerunRun(client, repo, run, opts.OnlyFailed)
if err != nil {
return err
}
if opts.IO.IsStdoutTTY() {
onlyFailedMsg := ""
if opts.OnlyFailed {
onlyFailedMsg = "(failed jobs) "
}
fmt.Fprintf(opts.IO.Out, "%s Requested rerun %sof run %s\n",
cs.SuccessIcon(),
onlyFailedMsg,
cs.Cyanf("%d", run.ID))
}
}
return nil
}
func rerunRun(client *api.Client, repo ghrepo.Interface, run *shared.Run, onlyFailed bool) error {
runVerb := "rerun"
if onlyFailed {
runVerb = "rerun-failed-jobs"
}
path := fmt.Sprintf("repos/%s/actions/runs/%d/%s", ghrepo.FullName(repo), run.ID, runVerb)
err := client.REST(repo.RepoHost(), "POST", path, nil, nil)
if err != nil {
var httpError api.HTTPError
if errors.As(err, &httpError) && httpError.StatusCode == 403 {
return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken", run.ID)
}
return fmt.Errorf("failed to rerun: %w", err)
}
return nil
}
func rerunJob(client *api.Client, repo ghrepo.Interface, job *shared.Job) error {
path := fmt.Sprintf("repos/%s/actions/jobs/%d/rerun", ghrepo.FullName(repo), job.ID)
err := client.REST(repo.RepoHost(), "POST", path, nil, nil)
if err != nil {
var httpError api.HTTPError
if errors.As(err, &httpError) && httpError.StatusCode == 403 {
return fmt.Errorf("job %d cannot be rerun", job.ID)
}
return fmt.Errorf("failed to rerun: %w", err)
}
return nil
}

View file

@ -50,6 +50,48 @@ func TestNewCmdRerun(t *testing.T) {
RunID: "1234",
},
},
{
name: "failed arg nontty",
cli: "4321 --failed",
wants: RerunOptions{
RunID: "4321",
OnlyFailed: true,
},
},
{
name: "failed arg",
tty: true,
cli: "--failed",
wants: RerunOptions{
Prompt: true,
OnlyFailed: true,
},
},
{
name: "with arg job",
tty: true,
cli: "--job 1234",
wants: RerunOptions{
JobID: "1234",
},
},
{
name: "with args jobID and runID fails",
tty: true,
cli: "1234 --job 5678",
wantsErr: true,
},
{
name: "with arg job with no ID fails",
tty: true,
cli: "--job",
wantsErr: true,
},
{
name: "with arg job with no ID no tty fails",
cli: "--job",
wantsErr: true,
},
}
for _, tt := range tests {
@ -117,6 +159,39 @@ func TestRerun(t *testing.T) {
},
wantOut: "✓ Requested rerun of run 1234\n",
},
{
name: "arg including onlyFailed",
tty: true,
opts: &RerunOptions{
RunID: "1234",
OnlyFailed: true,
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
httpmock.JSONResponse(shared.FailedRun))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"),
httpmock.StringResponse("{}"))
},
wantOut: "✓ Requested rerun (failed jobs) of run 1234\n",
},
{
name: "arg including a specific job",
tty: true,
opts: &RerunOptions{
JobID: "20", // 20 is shared.FailedJob.ID
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"),
httpmock.JSONResponse(shared.FailedJob))
reg.Register(
httpmock.REST("POST", "repos/OWNER/REPO/actions/jobs/20/rerun"),
httpmock.StringResponse("{}"))
},
wantOut: "✓ Requested rerun of job 20 on run 1234\n",
},
{
name: "prompt",
tty: true,
@ -158,7 +233,7 @@ func TestRerun(t *testing.T) {
}}))
},
wantErr: true,
errOut: "no recent runs have failed; please specify a specific run ID",
errOut: "no recent runs have failed; please specify a specific `<run-id>`",
},
{
name: "unrerunnable",
@ -175,7 +250,7 @@ func TestRerun(t *testing.T) {
httpmock.StatusStringResponse(403, "no"))
},
wantErr: true,
errOut: "run 3 cannot be rerun; its workflow file may be broken.",
errOut: "run 3 cannot be rerun; its workflow file may be broken",
},
}

View file

@ -307,6 +307,18 @@ func GetJobs(client *api.Client, repo ghrepo.Interface, run Run) ([]Job, error)
return result.Jobs, nil
}
func GetJob(client *api.Client, repo ghrepo.Interface, jobID string) (*Job, error) {
path := fmt.Sprintf("repos/%s/actions/jobs/%s", ghrepo.FullName(repo), jobID)
var result Job
err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) {
var selected int
now := time.Now()

View file

@ -183,7 +183,7 @@ func runView(opts *ViewOptions) error {
if jobID != "" {
opts.IO.StartProgressIndicator()
selectedJob, err = getJob(client, repo, jobID)
selectedJob, err = shared.GetJob(client, repo, jobID)
opts.IO.StopProgressIndicator()
if err != nil {
return fmt.Errorf("failed to get job: %w", err)
@ -395,18 +395,6 @@ func runView(opts *ViewOptions) error {
return nil
}
func getJob(client *api.Client, repo ghrepo.Interface, jobID string) (*shared.Job, error) {
path := fmt.Sprintf("repos/%s/actions/jobs/%s", ghrepo.FullName(repo), jobID)
var result shared.Job
err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func getLog(httpClient *http.Client, logURL string) (io.ReadCloser, error) {
req, err := http.NewRequest("GET", logURL, nil)
if err != nil {

View file

@ -0,0 +1,203 @@
package repos
import (
"fmt"
"strings"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/search"
"github.com/cli/cli/v2/pkg/text"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
)
const (
// Limitation of GitHub search see:
// https://docs.github.com/en/rest/reference/search
searchMaxResults = 1000
)
type ReposOptions struct {
Browser cmdutil.Browser
Exporter cmdutil.Exporter
IO *iostreams.IOStreams
Query search.Query
Searcher search.Searcher
WebMode bool
}
func NewCmdRepos(f *cmdutil.Factory, runF func(*ReposOptions) error) *cobra.Command {
var order string
var sort string
opts := &ReposOptions{
Browser: f.Browser,
IO: f.IOStreams,
Query: search.Query{Kind: search.KindRepositories},
}
cmd := &cobra.Command{
Use: "repos [<query>]",
Short: "Search for repositories",
Long: heredoc.Doc(`
Search for repositories on GitHub.
The command supports constructing queries using the GitHub search syntax,
using the parameter and qualifier flags, or a combination of the two.
GitHub search syntax is documented at:
https://docs.github.com/search-github/searching-on-github/searching-for-repositories
`),
Example: heredoc.Doc(`
# search repositories matching set of keywords "cli" and "shell"
$ gh search repos cli shell
# search repositories matching phrase "vim plugin"
$ gh search repos "vim plugin"
# search repositories public repos in the microsoft organization
$ gh search repos --owner=microsoft --visibility=public
# search repositories with a set of topics
$ gh search repos --topic=unix,terminal
# search repositories by coding language and number of good first issues
$ gh search repos --language=go --good-first-issues=">=10"
`),
RunE: func(c *cobra.Command, args []string) error {
if len(args) == 0 && c.Flags().NFlag() == 0 {
return cmdutil.FlagErrorf("specify search keywords or flags")
}
if opts.Query.Limit < 1 || opts.Query.Limit > searchMaxResults {
return cmdutil.FlagErrorf("`--limit` must be between 1 and 1000")
}
if c.Flags().Changed("order") {
opts.Query.Order = order
}
if c.Flags().Changed("sort") {
opts.Query.Sort = sort
}
opts.Query.Keywords = args
if runF != nil {
return runF(opts)
}
var err error
opts.Searcher, err = searcher(f)
if err != nil {
return err
}
return reposRun(opts)
},
}
// Output flags
cmdutil.AddJSONFlags(cmd, &opts.Exporter, search.RepositoryFields)
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the search query in the web browser")
// Query parameter flags
cmd.Flags().IntVarP(&opts.Query.Limit, "limit", "L", 30, "Maximum number of repositories to fetch")
cmdutil.StringEnumFlag(cmd, &order, "order", "", "desc", []string{"asc", "desc"}, "Order of repositories returned, ignored unless '--sort' flag is specified")
cmdutil.StringEnumFlag(cmd, &sort, "sort", "", "best-match", []string{"forks", "help-wanted-issues", "stars", "updated"}, "Sort fetched repositories")
// Query qualifier flags
cmdutil.NilBoolFlag(cmd, &opts.Query.Qualifiers.Archived, "archived", "", "Filter based on archive state")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Created, "created", "", "Filter based on created at `date`")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Followers, "followers", "", "Filter based on `number` of followers")
cmdutil.StringEnumFlag(cmd, &opts.Query.Qualifiers.Fork, "include-forks", "", "", []string{"false", "true", "only"}, "Include forks in fetched repositories")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Forks, "forks", "", "Filter on `number` of forks")
cmd.Flags().StringVar(&opts.Query.Qualifiers.GoodFirstIssues, "good-first-issues", "", "Filter on `number` of issues with the 'good first issue' label")
cmd.Flags().StringVar(&opts.Query.Qualifiers.HelpWantedIssues, "help-wanted-issues", "", "Filter on `number` of issues with the 'help wanted' label")
cmdutil.StringSliceEnumFlag(cmd, &opts.Query.Qualifiers.In, "match", "", nil, []string{"name", "description", "readme"}, "Restrict search to specific field of repository")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Language, "language", "", "Filter based on the coding language")
cmd.Flags().StringSliceVar(&opts.Query.Qualifiers.License, "license", nil, "Filter based on license type")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Org, "owner", "", "Filter on owner")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Pushed, "updated", "", "Filter on last updated at `date`")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Size, "size", "", "Filter on a size range, in kilobytes")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Stars, "stars", "", "Filter on `number` of stars")
cmd.Flags().StringSliceVar(&opts.Query.Qualifiers.Topic, "topic", nil, "Filter on topic")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Topics, "number-topics", "", "Filter on `number` of topics")
cmdutil.StringEnumFlag(cmd, &opts.Query.Qualifiers.Is, "visibility", "", "", []string{"public", "private", "internal"}, "Filter based on visibility")
return cmd
}
func reposRun(opts *ReposOptions) error {
io := opts.IO
if opts.WebMode {
url := opts.Searcher.URL(opts.Query)
if io.IsStdoutTTY() {
fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(url))
}
return opts.Browser.Browse(url)
}
io.StartProgressIndicator()
result, err := opts.Searcher.Repositories(opts.Query)
io.StopProgressIndicator()
if err != nil {
return err
}
if err := io.StartPager(); err == nil {
defer io.StopPager()
} else {
fmt.Fprintf(io.ErrOut, "failed to start pager: %v\n", err)
}
if opts.Exporter != nil {
return opts.Exporter.Write(io, result.Items)
}
return displayResults(io, result)
}
func displayResults(io *iostreams.IOStreams, results search.RepositoriesResult) error {
cs := io.ColorScheme()
tp := utils.NewTablePrinter(io)
for _, repo := range results.Items {
tags := []string{repo.Visibility}
if repo.IsFork {
tags = append(tags, "fork")
}
if repo.IsArchived {
tags = append(tags, "archived")
}
info := strings.Join(tags, ", ")
infoColor := cs.Gray
if repo.IsPrivate {
infoColor = cs.Yellow
}
tp.AddField(repo.FullName, nil, cs.Bold)
description := repo.Description
tp.AddField(text.ReplaceExcessiveWhitespace(description), nil, nil)
tp.AddField(info, nil, infoColor)
if tp.IsTTY() {
tp.AddField(utils.FuzzyAgoAbbr(time.Now(), repo.UpdatedAt), nil, cs.Gray)
} else {
tp.AddField(repo.UpdatedAt.Format(time.RFC3339), nil, nil)
}
tp.EndRow()
}
if io.IsStdoutTTY() {
header := "No repositories matched your search\n"
if len(results.Items) > 0 {
header = fmt.Sprintf("Showing %d of %d repositories\n\n", len(results.Items), results.Total)
}
fmt.Fprintf(io.Out, "\n%s", header)
}
return tp.Render()
}
func searcher(f *cmdutil.Factory) (search.Searcher, error) {
cfg, err := f.Config()
if err != nil {
return nil, err
}
host, err := cfg.DefaultHost()
if err != nil {
return nil, err
}
client, err := f.HttpClient()
if err != nil {
return nil, err
}
return search.NewSearcher(client, host), nil
}

View file

@ -0,0 +1,295 @@
package repos
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/search"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func TestNewCmdRepos(t *testing.T) {
var trueBool = true
tests := []struct {
name string
input string
output ReposOptions
wantErr bool
errMsg string
}{
{
name: "no arguments",
input: "",
wantErr: true,
errMsg: "specify search keywords or flags",
},
{
name: "keyword arguments",
input: "some search terms",
output: ReposOptions{
Query: search.Query{Keywords: []string{"some", "search", "terms"}, Kind: "repositories", Limit: 30},
},
},
{
name: "web flag",
input: "--web",
output: ReposOptions{
Query: search.Query{Keywords: []string{}, Kind: "repositories", Limit: 30},
WebMode: true,
},
},
{
name: "limit flag",
input: "--limit 10",
output: ReposOptions{Query: search.Query{Keywords: []string{}, Kind: "repositories", Limit: 10}},
},
{
name: "invalid limit flag",
input: "--limit 1001",
wantErr: true,
errMsg: "`--limit` must be between 1 and 1000",
},
{
name: "order flag",
input: "--order asc",
output: ReposOptions{
Query: search.Query{Keywords: []string{}, Kind: "repositories", Limit: 30, Order: "asc"},
},
},
{
name: "invalid order flag",
input: "--order invalid",
wantErr: true,
errMsg: "invalid argument \"invalid\" for \"--order\" flag: valid values are {asc|desc}",
},
{
name: "qualifier flags",
input: `
--archived
--created=created
--followers=1
--include-forks=true
--forks=2
--good-first-issues=3
--help-wanted-issues=4
--match=description,readme
--language=language
--license=license
--owner=owner
--updated=updated
--size=5
--stars=6
--topic=topic
--number-topics=7
--visibility=public
`,
output: ReposOptions{
Query: search.Query{
Keywords: []string{},
Kind: "repositories",
Limit: 30,
Qualifiers: search.Qualifiers{
Archived: &trueBool,
Created: "created",
Followers: "1",
Fork: "true",
Forks: "2",
GoodFirstIssues: "3",
HelpWantedIssues: "4",
In: []string{"description", "readme"},
Language: "language",
License: []string{"license"},
Org: "owner",
Pushed: "updated",
Size: "5",
Stars: "6",
Topic: []string{"topic"},
Topics: "7",
Is: "public",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: io,
}
argv, err := shlex.Split(tt.input)
assert.NoError(t, err)
var gotOpts *ReposOptions
cmd := NewCmdRepos(f, func(opts *ReposOptions) error {
gotOpts = opts
return nil
})
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantErr {
assert.EqualError(t, err, tt.errMsg)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.output.Query, gotOpts.Query)
assert.Equal(t, tt.output.WebMode, gotOpts.WebMode)
})
}
}
func Test_ReposRun(t *testing.T) {
var query = search.Query{
Keywords: []string{"cli"},
Kind: "repositories",
Limit: 30,
Qualifiers: search.Qualifiers{
Stars: ">50",
Topic: []string{"golang"},
},
}
var updatedAt = time.Date(2021, 2, 28, 12, 30, 0, 0, time.UTC)
tests := []struct {
errMsg string
name string
opts *ReposOptions
tty bool
wantErr bool
wantStderr string
wantStdout string
}{
{
name: "displays results tty",
opts: &ReposOptions{
Query: query,
Searcher: &search.SearcherMock{
RepositoriesFunc: func(query search.Query) (search.RepositoriesResult, error) {
return search.RepositoriesResult{
IncompleteResults: false,
Items: []search.Repository{
{FullName: "test/cli", Description: "of course", IsPrivate: true, IsArchived: true, UpdatedAt: updatedAt, Visibility: "private"},
{FullName: "test/cliing", Description: "wow", IsFork: true, UpdatedAt: updatedAt, Visibility: "public"},
{FullName: "cli/cli", Description: "so much", IsArchived: false, UpdatedAt: updatedAt, Visibility: "internal"},
},
Total: 300,
}, nil
},
},
},
tty: true,
wantStdout: "\nShowing 3 of 300 repositories\n\ntest/cli of course private, archived Feb 28, 2021\ntest/cliing wow public, fork Feb 28, 2021\ncli/cli so much internal Feb 28, 2021\n",
},
{
name: "displays no results tty",
opts: &ReposOptions{
Query: query,
Searcher: &search.SearcherMock{
RepositoriesFunc: func(query search.Query) (search.RepositoriesResult, error) {
return search.RepositoriesResult{}, nil
},
},
},
tty: true,
wantStdout: "\nNo repositories matched your search\n",
},
{
name: "displays results notty",
opts: &ReposOptions{
Query: query,
Searcher: &search.SearcherMock{
RepositoriesFunc: func(query search.Query) (search.RepositoriesResult, error) {
return search.RepositoriesResult{
IncompleteResults: false,
Items: []search.Repository{
{FullName: "test/cli", Description: "of course", IsPrivate: true, IsArchived: true, UpdatedAt: updatedAt, Visibility: "private"},
{FullName: "test/cliing", Description: "wow", IsFork: true, UpdatedAt: updatedAt, Visibility: "public"},
{FullName: "cli/cli", Description: "so much", IsArchived: false, UpdatedAt: updatedAt, Visibility: "internal"},
},
Total: 300,
}, nil
},
},
},
wantStdout: "test/cli\tof course\tprivate, archived\t2021-02-28T12:30:00Z\ntest/cliing\twow\tpublic, fork\t2021-02-28T12:30:00Z\ncli/cli\tso much\tinternal\t2021-02-28T12:30:00Z\n",
},
{
name: "displays no results notty",
opts: &ReposOptions{
Query: query,
Searcher: &search.SearcherMock{
RepositoriesFunc: func(query search.Query) (search.RepositoriesResult, error) {
return search.RepositoriesResult{}, nil
},
},
},
},
{
name: "displays search error",
opts: &ReposOptions{
Query: query,
Searcher: &search.SearcherMock{
RepositoriesFunc: func(query search.Query) (search.RepositoriesResult, error) {
return search.RepositoriesResult{}, fmt.Errorf("error with query")
},
},
},
errMsg: "error with query",
wantErr: true,
},
{
name: "opens browser for web mode tty",
opts: &ReposOptions{
Browser: &cmdutil.TestBrowser{},
Query: query,
Searcher: &search.SearcherMock{
URLFunc: func(query search.Query) string {
return "https://github.com/search?type=repositories&q=cli"
},
},
WebMode: true,
},
tty: true,
wantStderr: "Opening github.com/search in your browser.\n",
},
{
name: "opens browser for web mode notty",
opts: &ReposOptions{
Browser: &cmdutil.TestBrowser{},
Query: query,
Searcher: &search.SearcherMock{
URLFunc: func(query search.Query) string {
return "https://github.com/search?type=repositories&q=cli"
},
},
WebMode: true,
},
},
}
for _, tt := range tests {
io, _, stdout, stderr := iostreams.Test()
io.SetStdinTTY(tt.tty)
io.SetStdoutTTY(tt.tty)
io.SetStderrTTY(tt.tty)
tt.opts.IO = io
t.Run(tt.name, func(t *testing.T) {
err := reposRun(tt.opts)
if tt.wantErr {
assert.EqualError(t, err, tt.errMsg)
return
} else if err != nil {
t.Fatalf("reposRun unexpected error: %v", err)
}
assert.Equal(t, tt.wantStdout, stdout.String())
assert.Equal(t, tt.wantStderr, stderr.String())
})
}
}

20
pkg/cmd/search/search.go Normal file
View file

@ -0,0 +1,20 @@
package search
import (
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
searchReposCmd "github.com/cli/cli/v2/pkg/cmd/search/repos"
)
func NewCmdSearch(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "search <command>",
Short: "Search for repositories, issues, pull requests and users",
Long: "Search across all of GitHub.",
}
cmd.AddCommand(searchReposCmd.NewCmdRepos(f, nil))
return cmd
}

View file

@ -48,7 +48,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
- organization: available to Actions runs within an organization
- user: available to Codespaces for your user
`),
Args: cobra.NoArgs,
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo

View file

@ -14,8 +14,9 @@ func NewCmdSecret(f *cmdutil.Factory) *cobra.Command {
Use: "secret <command>",
Short: "Manage GitHub secrets",
Long: heredoc.Doc(`
Secrets can be set at the repository, environment, or organization level for use in
GitHub Actions. User secrets can be set for use in GitHub Codespaces.
Secrets can be set at the repository, or organization level for use in
GitHub Actions or Dependabot. User secrets can be set for use in GitHub Codespaces.
Environment secrets can be set for use in GitHub Actions.
Run "gh help secret set" to learn how to get started.
`),
}

View file

@ -10,6 +10,7 @@ import (
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/secret/shared"
)
type SecretPayload struct {
@ -40,17 +41,17 @@ func getPubKey(client *api.Client, host, path string) (*PubKey, error) {
return &pk, nil
}
func getOrgPublicKey(client *api.Client, host, orgName string) (*PubKey, error) {
return getPubKey(client, host, fmt.Sprintf("orgs/%s/actions/secrets/public-key", orgName))
func getOrgPublicKey(client *api.Client, host, orgName string, app shared.App) (*PubKey, error) {
return getPubKey(client, host, fmt.Sprintf("orgs/%s/%s/secrets/public-key", orgName, app))
}
func getUserPublicKey(client *api.Client, host string) (*PubKey, error) {
return getPubKey(client, host, "user/codespaces/secrets/public-key")
}
func getRepoPubKey(client *api.Client, repo ghrepo.Interface) (*PubKey, error) {
return getPubKey(client, repo.RepoHost(), fmt.Sprintf("repos/%s/actions/secrets/public-key",
ghrepo.FullName(repo)))
func getRepoPubKey(client *api.Client, repo ghrepo.Interface, app shared.App) (*PubKey, error) {
return getPubKey(client, repo.RepoHost(), fmt.Sprintf("repos/%s/%s/secrets/public-key",
ghrepo.FullName(repo), app))
}
func getEnvPubKey(client *api.Client, repo ghrepo.Interface, envName string) (*PubKey, error) {
@ -68,14 +69,14 @@ func putSecret(client *api.Client, host, path string, payload interface{}) error
return client.REST(host, "PUT", path, requestBody, nil)
}
func putOrgSecret(client *api.Client, host string, pk *PubKey, orgName, visibility, secretName, eValue string, repositoryIDs []int64) error {
func putOrgSecret(client *api.Client, host string, pk *PubKey, orgName, visibility, secretName, eValue string, repositoryIDs []int64, app shared.App) error {
payload := SecretPayload{
EncryptedValue: eValue,
KeyID: pk.ID,
Repositories: repositoryIDs,
Visibility: visibility,
}
path := fmt.Sprintf("orgs/%s/actions/secrets/%s", orgName, secretName)
path := fmt.Sprintf("orgs/%s/%s/secrets/%s", orgName, app, secretName)
return putSecret(client, host, path, payload)
}
@ -107,12 +108,12 @@ func putEnvSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, envName
return putSecret(client, repo.RepoHost(), path, payload)
}
func putRepoSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, secretName, eValue string) error {
func putRepoSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, secretName, eValue string, app shared.App) error {
payload := SecretPayload{
EncryptedValue: eValue,
KeyID: pk.ID,
}
path := fmt.Sprintf("repos/%s/actions/secrets/%s", ghrepo.FullName(repo), secretName)
path := fmt.Sprintf("repos/%s/%s/secrets/%s", ghrepo.FullName(repo), app, secretName)
return putSecret(client, repo.RepoHost(), path, payload)
}

View file

@ -42,6 +42,7 @@ type SetOptions struct {
Visibility string
RepositoryNames []string
EnvFile string
Application string
}
func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command {
@ -56,9 +57,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
Short: "Create or update secrets",
Long: heredoc.Doc(`
Set a value for a secret on one of the following levels:
- repository (default): available to Actions runs in a repository
- repository (default): available to Actions runs or Dependabot in a repository
- environment: available to Actions runs for a deployment environment in a repository
- organization: available to Actions runs within an organization
- organization: available to Actions runs or Dependabot within an organization
- user: available to Codespaces for your user
Organization and user secrets can optionally be restricted to only be available to
@ -88,6 +89,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
# Set user-level secret for Codespaces
$ gh secret set MYSECRET --user
# Set repository-level secret for Dependabot
$ gh secret set MYSECRET --app dependabot
# Set multiple secrets imported from the ".env" file
$ gh secret set -f .env
`),
@ -121,10 +125,6 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
return cmdutil.FlagErrorf("`--visibility` is only supported with `--org`")
}
if opts.Visibility != shared.All && opts.Visibility != shared.Private && opts.Visibility != shared.Selected {
return cmdutil.FlagErrorf("`--visibility` must be one of \"all\", \"private\", or \"selected\"")
}
if opts.Visibility != shared.Selected && len(opts.RepositoryNames) > 0 {
return cmdutil.FlagErrorf("`--repos` is only supported with `--visibility=selected`")
}
@ -149,11 +149,12 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Set `organization` secret")
cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Set deployment `environment` secret")
cmd.Flags().BoolVarP(&opts.UserSecrets, "user", "u", false, "Set a secret for your user")
cmd.Flags().StringVarP(&opts.Visibility, "visibility", "v", "private", "Set visibility for an organization secret: `{all|private|selected}`")
cmdutil.StringEnumFlag(cmd, &opts.Visibility, "visibility", "v", shared.Private, []string{shared.All, shared.Private, shared.Selected}, "Set visibility for an organization secret")
cmd.Flags().StringSliceVarP(&opts.RepositoryNames, "repos", "r", []string{}, "List of `repositories` that can access an organization or user secret")
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "The value for the secret (reads from standard input if not specified)")
cmd.Flags().BoolVar(&opts.DoNotStore, "no-store", false, "Print the encrypted, base64-encoded value instead of storing it on Github")
cmd.Flags().StringVarP(&opts.EnvFile, "env-file", "f", "", "Load secret names and values from a dotenv-formatted `file`")
cmdutil.StringEnumFlag(cmd, &opts.Application, "app", "a", "", []string{shared.Actions, shared.Codespaces, shared.Dependabot}, "Set the application for a secret")
return cmd
}
@ -193,6 +194,35 @@ func setRun(opts *SetOptions) error {
}
}
secretEntity, err := shared.GetSecretEntity(orgName, envName, opts.UserSecrets)
if err != nil {
return err
}
secretApp, err := shared.GetSecretApp(opts.Application, secretEntity)
if err != nil {
return err
}
if !shared.IsSupportedSecretEntity(secretApp, secretEntity) {
return fmt.Errorf("%s secrets are not supported for %s", secretEntity, secretApp)
}
var pk *PubKey
switch secretEntity {
case shared.Organization:
pk, err = getOrgPublicKey(client, host, orgName, secretApp)
case shared.Environment:
pk, err = getEnvPubKey(client, baseRepo, envName)
case shared.User:
pk, err = getUserPublicKey(client, host)
default:
pk, err = getRepoPubKey(client, baseRepo, secretApp)
}
if err != nil {
return fmt.Errorf("failed to fetch public key: %w", err)
}
type repoNamesResult struct {
ids []int64
err error
@ -210,20 +240,6 @@ func setRun(opts *SetOptions) error {
}
}()
var pk *PubKey
if orgName != "" {
pk, err = getOrgPublicKey(client, host, orgName)
} else if envName != "" {
pk, err = getEnvPubKey(client, baseRepo, envName)
} else if opts.UserSecrets {
pk, err = getUserPublicKey(client, host)
} else {
pk, err = getRepoPubKey(client, baseRepo)
}
if err != nil {
return fmt.Errorf("failed to fetch public key: %w", err)
}
var repositoryIDs []int64
if result := <-repoNamesC; result.err == nil {
repositoryIDs = result.ids
@ -236,7 +252,7 @@ func setRun(opts *SetOptions) error {
key := secretKey
value := secret
go func() {
setc <- setSecret(opts, pk, host, client, baseRepo, key, value, repositoryIDs)
setc <- setSecret(opts, pk, host, client, baseRepo, key, value, repositoryIDs, secretApp, secretEntity)
}()
}
@ -261,7 +277,7 @@ func setRun(opts *SetOptions) error {
} else if orgName == "" {
target = ghrepo.FullName(baseRepo)
}
fmt.Fprintf(opts.IO.Out, "%s Set secret %s for %s\n", cs.SuccessIcon(), result.key, target)
fmt.Fprintf(opts.IO.Out, "%s Set %s secret %s for %s\n", cs.SuccessIcon(), secretApp.Title(), result.key, target)
}
return err
}
@ -272,7 +288,7 @@ type setResult struct {
err error
}
func setSecret(opts *SetOptions, pk *PubKey, host string, client *api.Client, baseRepo ghrepo.Interface, secretKey string, secret []byte, repositoryIDs []int64) (res setResult) {
func setSecret(opts *SetOptions, pk *PubKey, host string, client *api.Client, baseRepo ghrepo.Interface, secretKey string, secret []byte, repositoryIDs []int64, app shared.App, entity shared.SecretEntity) (res setResult) {
orgName := opts.OrgName
envName := opts.EnvName
res.key = secretKey
@ -301,14 +317,15 @@ func setSecret(opts *SetOptions, pk *PubKey, host string, client *api.Client, ba
return
}
if orgName != "" {
err = putOrgSecret(client, host, pk, opts.OrgName, opts.Visibility, secretKey, encoded, repositoryIDs)
} else if envName != "" {
switch entity {
case shared.Organization:
err = putOrgSecret(client, host, pk, orgName, opts.Visibility, secretKey, encoded, repositoryIDs, app)
case shared.Environment:
err = putEnvSecret(client, pk, baseRepo, envName, secretKey, encoded)
} else if opts.UserSecrets {
case shared.User:
err = putUserSecret(client, host, pk, secretKey, encoded, repositoryIDs)
} else {
err = putRepoSecret(client, pk, baseRepo, secretKey, encoded)
default:
err = putRepoSecret(client, pk, baseRepo, secretKey, encoded, app)
}
if err != nil {
res.err = fmt.Errorf("failed to set secret %q: %w", secretKey, err)

View file

@ -143,6 +143,29 @@ func TestNewCmdSet(t *testing.T) {
DoNotStore: true,
},
},
{
name: "Dependabot repo",
cli: `cool_secret -b"a secret" --app Dependabot`,
wants: SetOptions{
SecretName: "cool_secret",
Visibility: shared.Private,
Body: "a secret",
OrgName: "",
Application: "Dependabot",
},
},
{
name: "Dependabot org",
cli: "-ocoolOrg -bs -vselected -rcoolRepo cool_secret -aDependabot",
wants: SetOptions{
SecretName: "cool_secret",
Visibility: shared.Selected,
RepositoryNames: []string{"coolRepo"},
Body: "s",
OrgName: "coolOrg",
Application: "Dependabot",
},
},
}
for _, tt := range tests {
@ -181,46 +204,81 @@ func TestNewCmdSet(t *testing.T) {
assert.Equal(t, tt.wants.EnvName, gotOpts.EnvName)
assert.Equal(t, tt.wants.DoNotStore, gotOpts.DoNotStore)
assert.ElementsMatch(t, tt.wants.RepositoryNames, gotOpts.RepositoryNames)
assert.Equal(t, tt.wants.Application, gotOpts.Application)
})
}
}
func Test_setRun_repo(t *testing.T) {
reg := &httpmock.Registry{}
reg.Register(httpmock.REST("GET", "repos/owner/repo/actions/secrets/public-key"),
httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))
reg.Register(httpmock.REST("PUT", "repos/owner/repo/actions/secrets/cool_secret"), httpmock.StatusStringResponse(201, `{}`))
io, _, _, _ := iostreams.Test()
opts := &SetOptions{
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
tests := []struct {
name string
opts *SetOptions
wantApp string
}{
{
name: "Actions",
opts: &SetOptions{
Application: "actions",
},
wantApp: "actions",
},
Config: func() (config.Config, error) { return config.NewBlankConfig(), nil },
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.FromFullName("owner/repo")
{
name: "Dependabot",
opts: &SetOptions{
Application: "dependabot",
},
wantApp: "dependabot",
},
{
name: "defaults to Actions",
opts: &SetOptions{
Application: "",
},
wantApp: "actions",
},
IO: io,
SecretName: "cool_secret",
Body: "a secret",
RandomOverride: fakeRandom,
}
err := setRun(opts)
assert.NoError(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
reg.Verify(t)
reg.Register(httpmock.REST("GET", fmt.Sprintf("repos/owner/repo/%s/secrets/public-key", tt.wantApp)),
httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))
data, err := ioutil.ReadAll(reg.Requests[1].Body)
assert.NoError(t, err)
var payload SecretPayload
err = json.Unmarshal(data, &payload)
assert.NoError(t, err)
assert.Equal(t, payload.KeyID, "123")
assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
reg.Register(httpmock.REST("PUT", fmt.Sprintf("repos/owner/repo/%s/secrets/cool_secret", tt.wantApp)),
httpmock.StatusStringResponse(201, `{}`))
io, _, _, _ := iostreams.Test()
opts := &SetOptions{
HttpClient: func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
},
Config: func() (config.Config, error) { return config.NewBlankConfig(), nil },
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.FromFullName("owner/repo")
},
IO: io,
SecretName: "cool_secret",
Body: "a secret",
RandomOverride: fakeRandom,
Application: tt.opts.Application,
}
err := setRun(opts)
assert.NoError(t, err)
reg.Verify(t)
data, err := ioutil.ReadAll(reg.Requests[1].Body)
assert.NoError(t, err)
var payload SecretPayload
err = json.Unmarshal(data, &payload)
assert.NoError(t, err)
assert.Equal(t, payload.KeyID, "123")
assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
})
}
}
func Test_setRun_env(t *testing.T) {
@ -268,6 +326,7 @@ func Test_setRun_org(t *testing.T) {
opts *SetOptions
wantVisibility shared.Visibility
wantRepositories []int64
wantApp string
}{
{
name: "all vis",
@ -275,6 +334,7 @@ func Test_setRun_org(t *testing.T) {
OrgName: "UmbrellaCorporation",
Visibility: shared.All,
},
wantApp: "actions",
},
{
name: "selected visibility",
@ -284,6 +344,16 @@ func Test_setRun_org(t *testing.T) {
RepositoryNames: []string{"birkin", "UmbrellaCorporation/wesker"},
},
wantRepositories: []int64{1, 2},
wantApp: "actions",
},
{
name: "Dependabot",
opts: &SetOptions{
OrgName: "UmbrellaCorporation",
Visibility: shared.All,
Application: "dependabot",
},
wantApp: "dependabot",
},
}
@ -294,11 +364,11 @@ func Test_setRun_org(t *testing.T) {
orgName := tt.opts.OrgName
reg.Register(httpmock.REST("GET",
fmt.Sprintf("orgs/%s/actions/secrets/public-key", orgName)),
fmt.Sprintf("orgs/%s/%s/secrets/public-key", orgName, tt.wantApp)),
httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))
reg.Register(httpmock.REST("PUT",
fmt.Sprintf("orgs/%s/actions/secrets/cool_secret", orgName)),
fmt.Sprintf("orgs/%s/%s/secrets/cool_secret", orgName, tt.wantApp)),
httpmock.StatusStringResponse(201, `{}`))
if len(tt.opts.RepositoryNames) > 0 {

View file

@ -1,5 +1,11 @@
package shared
import (
"errors"
"fmt"
"strings"
)
type Visibility string
const (
@ -7,3 +13,80 @@ const (
Private = "private"
Selected = "selected"
)
type App string
const (
Actions = "actions"
Codespaces = "codespaces"
Dependabot = "dependabot"
Unknown = "unknown"
)
func (app App) String() string {
return string(app)
}
func (app App) Title() string {
return strings.Title(app.String())
}
type SecretEntity string
const (
Repository = "repository"
Organization = "organization"
User = "user"
Environment = "environment"
)
func GetSecretEntity(orgName, envName string, userSecrets bool) (SecretEntity, error) {
orgSet := orgName != ""
envSet := envName != ""
if orgSet && envSet || orgSet && userSecrets || envSet && userSecrets {
return "", errors.New("cannot specify multiple secret entities")
}
if orgSet {
return Organization, nil
}
if envSet {
return Environment, nil
}
if userSecrets {
return User, nil
}
return Repository, nil
}
func GetSecretApp(app string, entity SecretEntity) (App, error) {
switch strings.ToLower(app) {
case Actions:
return Actions, nil
case Codespaces:
return Codespaces, nil
case Dependabot:
return Dependabot, nil
case "":
if entity == User {
return Codespaces, nil
}
return Actions, nil
default:
return Unknown, fmt.Errorf("invalid application: %s", app)
}
}
func IsSupportedSecretEntity(app App, entity SecretEntity) bool {
switch app {
case Actions:
return entity == Repository || entity == Organization || entity == Environment
case Codespaces:
return entity == User
case Dependabot:
return entity == Repository || entity == Organization
default:
return false
}
}

View file

@ -0,0 +1,203 @@
package shared
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetSecretEntity(t *testing.T) {
tests := []struct {
name string
orgName string
envName string
userSecrets bool
want SecretEntity
wantErr bool
}{
{
name: "org",
orgName: "myOrg",
want: Organization,
},
{
name: "env",
envName: "myEnv",
want: Environment,
},
{
name: "user",
userSecrets: true,
want: User,
},
{
name: "defaults to repo",
want: Repository,
},
{
name: "Errors if both org and env are set",
orgName: "myOrg",
envName: "myEnv",
wantErr: true,
},
{
name: "Errors if both org and user secrets are set",
orgName: "myOrg",
userSecrets: true,
wantErr: true,
},
{
name: "Errors if both env and user secrets are set",
envName: "myEnv",
userSecrets: true,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
entity, err := GetSecretEntity(tt.orgName, tt.envName, tt.userSecrets)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, entity)
}
})
}
}
func TestGetSecretApp(t *testing.T) {
tests := []struct {
name string
app string
entity SecretEntity
want App
wantErr bool
}{
{
name: "Actions",
app: "actions",
want: Actions,
},
{
name: "Codespaces",
app: "codespaces",
want: Codespaces,
},
{
name: "Dependabot",
app: "dependabot",
want: Dependabot,
},
{
name: "Defaults to Actions for repository",
app: "",
entity: Repository,
want: Actions,
},
{
name: "Defaults to Actions for organization",
app: "",
entity: Organization,
want: Actions,
},
{
name: "Defaults to Actions for environment",
app: "",
entity: Environment,
want: Actions,
},
{
name: "Defaults to Codespaces for user",
app: "",
entity: User,
want: Codespaces,
},
{
name: "Unknown for invalid apps",
app: "invalid",
want: Unknown,
wantErr: true,
},
{
name: "case insensitive",
app: "ACTIONS",
want: Actions,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app, err := GetSecretApp(tt.app, tt.entity)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.want, app)
})
}
}
func TestIsSupportedSecretEntity(t *testing.T) {
tests := []struct {
name string
app App
supportedEntities []SecretEntity
unsupportedEntities []SecretEntity
}{
{
name: "Actions",
app: Actions,
supportedEntities: []SecretEntity{
Repository,
Organization,
Environment,
},
unsupportedEntities: []SecretEntity{
User,
Unknown,
},
},
{
name: "Codespaces",
app: Codespaces,
supportedEntities: []SecretEntity{
User,
},
unsupportedEntities: []SecretEntity{
Repository,
Organization,
Environment,
Unknown,
},
},
{
name: "Dependabot",
app: Dependabot,
supportedEntities: []SecretEntity{
Repository,
Organization,
},
unsupportedEntities: []SecretEntity{
Environment,
User,
Unknown,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, entity := range tt.supportedEntities {
assert.True(t, IsSupportedSecretEntity(tt.app, entity))
}
for _, entity := range tt.unsupportedEntities {
assert.False(t, IsSupportedSecretEntity(tt.app, entity))
}
})
}
}

View file

@ -26,9 +26,10 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
}
cmd := &cobra.Command{
Use: "list",
Short: "Lists SSH keys in your GitHub account",
Args: cobra.ExactArgs(0),
Use: "list",
Short: "Lists SSH keys in your GitHub account",
Aliases: []string{"ls"},
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
if runF != nil {
return runF(opts)

View file

@ -33,10 +33,11 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
}
cmd := &cobra.Command{
Use: "list",
Short: "List workflows",
Long: "List workflow files, hiding disabled workflows by default.",
Args: cobra.NoArgs,
Use: "list",
Short: "List workflows",
Long: "List workflow files, hiding disabled workflows by default.",
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo

View file

@ -1,7 +1,9 @@
package cmdutil
import (
"fmt"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@ -21,6 +23,31 @@ func NilBoolFlag(cmd *cobra.Command, p **bool, name string, shorthand string, us
return f
}
// StringEnumFlag defines a new string flag that only allows values listed in options.
func StringEnumFlag(cmd *cobra.Command, p *string, name, shorthand, defaultValue string, options []string, usage string) *pflag.Flag {
*p = defaultValue
val := &enumValue{string: p, options: options}
f := cmd.Flags().VarPF(val, name, shorthand, fmt.Sprintf("%s: %s", usage, formatValuesForUsageDocs(options)))
_ = cmd.RegisterFlagCompletionFunc(name, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return options, cobra.ShellCompDirectiveNoFileComp
})
return f
}
func StringSliceEnumFlag(cmd *cobra.Command, p *[]string, name, shorthand string, defaultValues, options []string, usage string) *pflag.Flag {
*p = defaultValues
val := &enumMultiValue{value: p, options: options}
f := cmd.Flags().VarPF(val, name, shorthand, fmt.Sprintf("%s: %s", usage, formatValuesForUsageDocs(options)))
_ = cmd.RegisterFlagCompletionFunc(name, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return options, cobra.ShellCompDirectiveNoFileComp
})
return f
}
func formatValuesForUsageDocs(values []string) string {
return fmt.Sprintf("{%s}", strings.Join(values, "|"))
}
type stringValue struct {
string **string
}
@ -75,3 +102,60 @@ func (b *boolValue) Type() string {
func (b *boolValue) IsBoolFlag() bool {
return true
}
type enumValue struct {
string *string
options []string
}
func (e *enumValue) Set(value string) error {
if !isIncluded(value, e.options) {
return fmt.Errorf("valid values are %s", formatValuesForUsageDocs(e.options))
}
*e.string = value
return nil
}
func (e *enumValue) String() string {
return *e.string
}
func (e *enumValue) Type() string {
return "string"
}
type enumMultiValue struct {
value *[]string
options []string
}
func (e *enumMultiValue) Set(value string) error {
items := strings.Split(value, ",")
for _, item := range items {
if !isIncluded(item, e.options) {
return fmt.Errorf("valid values are %s", formatValuesForUsageDocs(e.options))
}
}
*e.value = append(*e.value, items...)
return nil
}
func (e *enumMultiValue) String() string {
if len(*e.value) == 0 {
return ""
}
return fmt.Sprintf("{%s}", strings.Join(*e.value, ", "))
}
func (e *enumMultiValue) Type() string {
return "stringSlice"
}
func isIncluded(value string, opts []string) bool {
for _, opt := range opts {
if strings.EqualFold(opt, value) {
return true
}
}
return false
}

View file

@ -20,6 +20,7 @@ type Extension interface {
Path() string // Path to executable
URL() string
IsLocal() bool
CurrentVersion() string
UpdateAvailable() bool
IsBinary() bool
}

View file

@ -17,6 +17,9 @@ var _ Extension = &ExtensionMock{}
//
// // make and configure a mocked Extension
// mockedExtension := &ExtensionMock{
// CurrentVersionFunc: func() string {
// panic("mock out the CurrentVersion method")
// },
// IsBinaryFunc: func() bool {
// panic("mock out the IsBinary method")
// },
@ -42,6 +45,9 @@ var _ Extension = &ExtensionMock{}
//
// }
type ExtensionMock struct {
// CurrentVersionFunc mocks the CurrentVersion method.
CurrentVersionFunc func() string
// IsBinaryFunc mocks the IsBinary method.
IsBinaryFunc func() bool
@ -62,6 +68,9 @@ type ExtensionMock struct {
// calls tracks calls to the methods.
calls struct {
// CurrentVersion holds details about calls to the CurrentVersion method.
CurrentVersion []struct {
}
// IsBinary holds details about calls to the IsBinary method.
IsBinary []struct {
}
@ -81,6 +90,7 @@ type ExtensionMock struct {
UpdateAvailable []struct {
}
}
lockCurrentVersion sync.RWMutex
lockIsBinary sync.RWMutex
lockIsLocal sync.RWMutex
lockName sync.RWMutex
@ -89,6 +99,32 @@ type ExtensionMock struct {
lockUpdateAvailable sync.RWMutex
}
// CurrentVersion calls CurrentVersionFunc.
func (mock *ExtensionMock) CurrentVersion() string {
if mock.CurrentVersionFunc == nil {
panic("ExtensionMock.CurrentVersionFunc: method is nil but Extension.CurrentVersion was just called")
}
callInfo := struct {
}{}
mock.lockCurrentVersion.Lock()
mock.calls.CurrentVersion = append(mock.calls.CurrentVersion, callInfo)
mock.lockCurrentVersion.Unlock()
return mock.CurrentVersionFunc()
}
// CurrentVersionCalls gets all the calls that were made to CurrentVersion.
// Check the length with:
// len(mockedExtension.CurrentVersionCalls())
func (mock *ExtensionMock) CurrentVersionCalls() []struct {
} {
var calls []struct {
}
mock.lockCurrentVersion.RLock()
calls = mock.calls.CurrentVersion
mock.lockCurrentVersion.RUnlock()
return calls
}
// IsBinary calls IsBinaryFunc.
func (mock *ExtensionMock) IsBinary() bool {
if mock.IsBinaryFunc == nil {

View file

@ -6,6 +6,7 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"regexp"
"strings"
@ -56,6 +57,24 @@ func GraphQL(q string) Matcher {
}
}
func QueryMatcher(method string, path string, query url.Values) Matcher {
return func(req *http.Request) bool {
if !REST(method, path)(req) {
return false
}
actualQuery := req.URL.Query()
for param := range query {
if !(actualQuery.Get(param) == query.Get(param)) {
return false
}
}
return true
}
}
func readBody(req *http.Request) ([]byte, error) {
bodyCopy := &bytes.Buffer{}
r := io.TeeReader(req.Body, bodyCopy)

View file

@ -211,7 +211,7 @@ func (c *ColorScheme) ColorFromString(s string) func(string) string {
}
func (c *ColorScheme) HexToRGB(hex string, x string) string {
if !c.enabled || !c.hasTrueColor {
if !c.enabled || !c.hasTrueColor || len(hex) != 6 {
return x
}

View file

@ -175,6 +175,13 @@ func Test_HextoRGB(t *testing.T) {
wants: "red",
cs: NewColorScheme(false, false, false),
},
{
name: "invalid hex",
hex: "fc0",
text: "red",
wants: "red",
cs: NewColorScheme(false, false, false),
},
}
for _, tt := range tests {

111
pkg/search/query.go Normal file
View file

@ -0,0 +1,111 @@
package search
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/cli/cli/v2/pkg/text"
)
const (
KindRepositories = "repositories"
)
type Query struct {
Keywords []string
Kind string
Limit int
Order string
Page int
Qualifiers Qualifiers
Sort string
}
type Qualifiers struct {
Archived *bool
Created string
Followers string
Fork string
Forks string
GoodFirstIssues string
HelpWantedIssues string
In []string
Is string
Language string
License []string
Org string
Pushed string
Size string
Stars string
Topic []string
Topics string
}
func (q Query) String() string {
qualifiers := formatQualifiers(q.Qualifiers)
keywords := formatKeywords(q.Keywords)
all := append(keywords, qualifiers...)
return strings.Join(all, " ")
}
func (q Qualifiers) Map() map[string][]string {
m := map[string][]string{}
v := reflect.ValueOf(q)
t := reflect.TypeOf(q)
for i := 0; i < v.NumField(); i++ {
fieldName := t.Field(i).Name
key := text.CamelToKebab(fieldName)
typ := v.FieldByName(fieldName).Kind()
value := v.FieldByName(fieldName)
switch typ {
case reflect.Ptr:
if value.IsNil() {
continue
}
v := reflect.Indirect(value)
m[key] = []string{fmt.Sprintf("%v", v)}
case reflect.Slice:
if value.IsNil() {
continue
}
s := []string{}
for i := 0; i < value.Len(); i++ {
s = append(s, fmt.Sprintf("%v", value.Index(i)))
}
m[key] = s
default:
if value.IsZero() {
continue
}
m[key] = []string{fmt.Sprintf("%v", value)}
}
}
return m
}
func quote(s string) string {
if strings.ContainsAny(s, " \"\t\r\n") {
return fmt.Sprintf("%q", s)
}
return s
}
func formatQualifiers(qs Qualifiers) []string {
var all []string
for k, vs := range qs.Map() {
for _, v := range vs {
all = append(all, fmt.Sprintf("%s:%s", k, quote(v)))
}
}
sort.Strings(all)
return all
}
func formatKeywords(ks []string) []string {
for i, k := range ks {
ks[i] = quote(k)
}
return ks
}

135
pkg/search/query_test.go Normal file
View file

@ -0,0 +1,135 @@
package search
import (
"testing"
"github.com/stretchr/testify/assert"
)
var trueBool = true
func TestQueryString(t *testing.T) {
tests := []struct {
name string
query Query
out string
}{
{
name: "converts query to string",
query: Query{
Keywords: []string{"some", "keywords"},
Qualifiers: Qualifiers{
Archived: &trueBool,
Created: "created",
Followers: "1",
Fork: "true",
Forks: "2",
GoodFirstIssues: "3",
HelpWantedIssues: "4",
In: []string{"description", "readme"},
Language: "language",
License: []string{"license"},
Org: "org",
Pushed: "updated",
Size: "5",
Stars: "6",
Topic: []string{"topic"},
Topics: "7",
Is: "public",
},
},
out: "some keywords archived:true created:created followers:1 fork:true forks:2 good-first-issues:3 help-wanted-issues:4 in:description in:readme is:public language:language license:license org:org pushed:updated size:5 stars:6 topic:topic topics:7",
},
{
name: "quotes keywords",
query: Query{
Keywords: []string{"quote keywords"},
},
out: "\"quote keywords\"",
},
{
name: "quotes qualifiers",
query: Query{
Qualifiers: Qualifiers{
Topic: []string{"quote qualifier"},
},
},
out: "topic:\"quote qualifier\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.out, tt.query.String())
})
}
}
func TestQualifiersMap(t *testing.T) {
tests := []struct {
name string
qualifiers Qualifiers
out map[string][]string
}{
{
name: "changes qualifiers to map",
qualifiers: Qualifiers{
Archived: &trueBool,
Created: "created",
Followers: "1",
Fork: "true",
Forks: "2",
GoodFirstIssues: "3",
HelpWantedIssues: "4",
In: []string{"readme"},
Language: "language",
License: []string{"license"},
Org: "org",
Pushed: "updated",
Size: "5",
Stars: "6",
Topic: []string{"topic"},
Topics: "7",
Is: "public",
},
out: map[string][]string{
"archived": {"true"},
"created": {"created"},
"followers": {"1"},
"fork": {"true"},
"forks": {"2"},
"good-first-issues": {"3"},
"help-wanted-issues": {"4"},
"in": {"readme"},
"is": {"public"},
"language": {"language"},
"license": {"license"},
"org": {"org"},
"pushed": {"updated"},
"size": {"5"},
"stars": {"6"},
"topic": {"topic"},
"topics": {"7"},
},
},
{
name: "excludes unset qualifiers from map",
qualifiers: Qualifiers{
Org: "org",
Pushed: "updated",
Size: "5",
Stars: "6",
},
out: map[string][]string{
"org": {"org"},
"pushed": {"updated"},
"size": {"5"},
"stars": {"6"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.out, tt.qualifiers.Map())
})
}
}

120
pkg/search/result.go Normal file
View file

@ -0,0 +1,120 @@
package search
import (
"reflect"
"strings"
"time"
)
var RepositoryFields = []string{
"createdAt",
"defaultBranch",
"description",
"forksCount",
"fullName",
"hasDownloads",
"hasIssues",
"hasPages",
"hasProjects",
"hasWiki",
"homepage",
"id",
"isArchived",
"isDisabled",
"isFork",
"isPrivate",
"language",
"license",
"name",
"openIssuesCount",
"owner",
"pushedAt",
"size",
"stargazersCount",
"updatedAt",
"visibility",
"watchersCount",
}
type RepositoriesResult struct {
IncompleteResults bool `json:"incomplete_results"`
Items []Repository `json:"items"`
Total int `json:"total_count"`
}
type Repository struct {
CreatedAt time.Time `json:"created_at"`
DefaultBranch string `json:"default_branch"`
Description string `json:"description"`
ForksCount int `json:"forks_count"`
FullName string `json:"full_name"`
HasDownloads bool `json:"has_downloads"`
HasIssues bool `json:"has_issues"`
HasPages bool `json:"has_pages"`
HasProjects bool `json:"has_projects"`
HasWiki bool `json:"has_wiki"`
Homepage string `json:"homepage"`
ID int64 `json:"id"`
IsArchived bool `json:"archived"`
IsDisabled bool `json:"disabled"`
IsFork bool `json:"fork"`
IsPrivate bool `json:"private"`
Language string `json:"language"`
License License `json:"license"`
MasterBranch string `json:"master_branch"`
Name string `json:"name"`
OpenIssuesCount int `json:"open_issues_count"`
Owner User `json:"owner"`
PushedAt time.Time `json:"pushed_at"`
Size int `json:"size"`
StargazersCount int `json:"stargazers_count"`
UpdatedAt time.Time `json:"updated_at"`
Visibility string `json:"visibility"`
WatchersCount int `json:"watchers_count"`
}
type License struct {
HTMLURL string `json:"html_url"`
Key string `json:"key"`
Name string `json:"name"`
URL string `json:"url"`
}
type User struct {
GravatarID string `json:"gravatar_id"`
ID int64 `json:"id"`
Login string `json:"login"`
SiteAdmin bool `json:"site_admin"`
Type string `json:"type"`
}
func (repo Repository) ExportData(fields []string) map[string]interface{} {
v := reflect.ValueOf(repo)
data := map[string]interface{}{}
for _, f := range fields {
switch f {
case "license":
data[f] = map[string]interface{}{
"key": repo.License.Key,
"name": repo.License.Name,
"url": repo.License.URL,
}
case "owner":
data[f] = map[string]interface{}{
"id": repo.Owner.ID,
"login": repo.Owner.Login,
"type": repo.Owner.Type,
}
default:
sf := fieldByName(v, f)
data[f] = sf.Interface()
}
}
return data
}
func fieldByName(v reflect.Value, field string) reflect.Value {
return v.FieldByNameFunc(func(s string) bool {
return strings.EqualFold(field, s)
})
}

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