From 015b9f7fea68cbbeb324556f2101d57f73a544db Mon Sep 17 00:00:00 2001 From: keijun-kumagai Date: Tue, 11 Jan 2022 01:49:19 +0900 Subject: [PATCH 01/83] fix(release): discussion category with assets --- pkg/cmd/release/create/create.go | 7 ++++++- pkg/cmd/release/create/http.go | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index 1b834373f..bdfdf1460 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -418,7 +418,12 @@ func createRun(opts *CreateOptions) error { } if !opts.Draft { - rel, err := publishRelease(httpClient, newRelease.APIURL) + params := map[string]interface{}{} + params["draft"] = false + if opts.DiscussionCategory != "" { + params["discussion_category_name"] = opts.DiscussionCategory + } + rel, err := publishRelease(httpClient, newRelease.APIURL, params) if err != nil { return err } diff --git a/pkg/cmd/release/create/http.go b/pkg/cmd/release/create/http.go index fb082cd79..058b38c5b 100644 --- a/pkg/cmd/release/create/http.go +++ b/pkg/cmd/release/create/http.go @@ -134,8 +134,9 @@ 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, params map[string]interface{}) (*shared.Release, error) { + bodyBytes, err := json.Marshal(params) + req, err := http.NewRequest("PATCH", releaseURL, bytes.NewBuffer(bodyBytes)) if err != nil { return nil, err } From 6bc0b693554f5ab6d3761f5ee5c46ed2d7df3815 Mon Sep 17 00:00:00 2001 From: keijun-kumagai Date: Thu, 13 Jan 2022 00:33:07 +0900 Subject: [PATCH 02/83] feat(http): supress linter --- pkg/cmd/release/create/http.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cmd/release/create/http.go b/pkg/cmd/release/create/http.go index 058b38c5b..b5ee5c271 100644 --- a/pkg/cmd/release/create/http.go +++ b/pkg/cmd/release/create/http.go @@ -136,6 +136,9 @@ func createRelease(httpClient *http.Client, repo ghrepo.Interface, params map[st func publishRelease(httpClient *http.Client, releaseURL string, params map[string]interface{}) (*shared.Release, error) { 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 From ff6c7b925f99250b33618ad6249cc75177c76001 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Thu, 10 Feb 2022 13:53:55 -0800 Subject: [PATCH 03/83] Add flag to rerun only failed jobs in a workflow run --- pkg/cmd/run/rerun/rerun.go | 19 +++++++++++++++--- pkg/cmd/run/rerun/rerun_test.go | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 9788a3061..98cfa7060 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -18,7 +18,8 @@ type RerunOptions struct { IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) - RunID string + RunID string + OnlyFailed bool Prompt bool } @@ -52,6 +53,8 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm }, } + cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs") + return cmd } @@ -98,7 +101,12 @@ func runRerun(opts *RerunOptions) error { return fmt.Errorf("failed to get run: %w", err) } - path := fmt.Sprintf("repos/%s/actions/runs/%d/rerun", ghrepo.FullName(repo), run.ID) + runVerb := "rerun" + if opts.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 { @@ -111,8 +119,13 @@ func runRerun(opts *RerunOptions) error { if opts.IO.CanPrompt() { cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.Out, "%s Requested rerun of run %s\n", + 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)) } diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index 11362ce4b..3fcf68ff3 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -50,6 +50,23 @@ 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, + }, + }, } for _, tt := range tests { @@ -117,6 +134,23 @@ 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: "prompt", tty: true, From 32dada90bac83d0a4964a37704d02778124ff59c Mon Sep 17 00:00:00 2001 From: nate smith Date: Mon, 14 Feb 2022 10:35:09 -0600 Subject: [PATCH 04/83] revert PR 4998 --- docs/install_linux.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install_linux.md b/docs/install_linux.md index b45d7a599..b07e2478b 100644 --- a/docs/install_linux.md +++ b/docs/install_linux.md @@ -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 ``` From 28d2b52769421e82b4cc3a46a619216ddd1a923e Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Tue, 15 Feb 2022 16:36:37 +0100 Subject: [PATCH 05/83] release create: warn about unpushed local git tag (#5104) If `gh release create ` was called and TAG exists locally but not on the remote, this warns about the unpushed tag to avoid recreating it on the remote. The user can pass a value for `--target` to silence the warning. --- api/queries_repo.go | 3 +- pkg/cmd/release/create/create.go | 27 ++++++++++- pkg/cmd/release/create/create_test.go | 65 ++++++++++++++++++++++++++- pkg/cmd/release/create/http.go | 21 +++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 65f1027a8..b7ea14915 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -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" ) diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index 96f7245c1..4d0ea32c8 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -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 diff --git a/pkg/cmd/release/create/create_test.go b/pkg/cmd/release/create/create_test.go index ea9c2aa44..1811c30d7 100644 --- a/pkg/cmd/release/create/create_test.go +++ b/pkg/cmd/release/create/create_test.go @@ -673,6 +673,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 +692,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 +792,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, ¶ms) diff --git a/pkg/cmd/release/create/http.go b/pkg/cmd/release/create/http.go index fb082cd79..07a37a4f6 100644 --- a/pkg/cmd/release/create/http.go +++ b/pkg/cmd/release/create/http.go @@ -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 From 3e0db567e89fdc6fde158cfabee650187a34127d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 15 Feb 2022 17:23:39 +0100 Subject: [PATCH 06/83] Rotate our Windows signing certificates (#5196) - The certificate pfx file is now read from WINDOWS_CERT_PFX - The password to decode the pfx is in WINDOWS_CERT_PASSWORD - Quit reading from desktop-secrets repo - Switch osslsigncode to take in pfx instead of individual certs - :fire: obsolete setup scripts --- .github/workflows/releases.yml | 21 +++++++++++++------ .goreleaser.yml | 1 - script/prepare-windows-cert.sh | 19 ----------------- script/setup-windows-certificate.ps1 | 12 ----------- script/sign-windows-executable.sh | 31 ++++++++++++++-------------- script/sign.ps1 | 7 +------ 6 files changed, 31 insertions(+), 60 deletions(-) delete mode 100755 script/prepare-windows-cert.sh delete mode 100644 script/setup-windows-certificate.ps1 diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index b95c51715..6511fc71c 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -27,6 +27,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,8 +42,7 @@ 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 with: @@ -147,15 +153,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: | diff --git a/.goreleaser.yml b/.goreleaser.yml index 01c727d93..68d3dd9c9 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -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 diff --git a/script/prepare-windows-cert.sh b/script/prepare-windows-cert.sh deleted file mode 100755 index d52d12b1d..000000000 --- a/script/prepare-windows-cert.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e - -GITHUB_CERT_PASSWORD=$1 -DESKTOP_CERT_TOKEN=$2 - -if [[ -z "$GITHUB_CERT_PASSWORD" || -z "$DESKTOP_CERT_TOKEN" ]]; then - echo "skipping windows signing prep; cert password or token not found" - exit 0 -fi - -curl \ - -H "Authorization: token $DESKTOP_CERT_TOKEN" \ - -H "Accept: application/vnd.github.v3.raw" \ - --output windows-certificate.pfx \ - https://api.github.com/repos/desktop/desktop-secrets/contents/windows-certificate.pfx - -openssl pkcs12 -in windows-certificate.pfx -nocerts -nodes -out private-key.pem -passin pass:${GITHUB_CERT_PASSWORD} -openssl pkcs12 -in windows-certificate.pfx -nokeys -nodes -out certificate.pem -passin pass:${GITHUB_CERT_PASSWORD} diff --git a/script/setup-windows-certificate.ps1 b/script/setup-windows-certificate.ps1 deleted file mode 100644 index 9238fe67b..000000000 --- a/script/setup-windows-certificate.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition -$certFile = "$scriptPath\windows-certificate.pfx" - -$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" -$headers.Add("Authorization", "token $env:DESKTOP_CERT_TOKEN") -$headers.Add("Accept", 'application/vnd.github.v3.raw') - -Invoke-WebRequest 'https://api.github.com/repos/desktop/desktop-secrets/contents/windows-certificate.pfx' ` - -Headers $headers ` - -OutFile "$certFile" - -Write-Output "::set-output name=cert-file::$certFile" diff --git a/script/sign-windows-executable.sh b/script/sign-windows-executable.sh index 2141c9552..d89e6dbe4 100755 --- a/script/sign-windows-executable.sh +++ b/script/sign-windows-executable.sh @@ -1,26 +1,25 @@ #!/bin/bash set -e -if [[ ! -e certificate.pem || ! -e private-key.pem ]]; then - echo "skipping windows signing; cert or key not found" +EXE="$1" + +if [ -z "$CERT_FILE" ]; then + echo "skipping Windows code-signing; CERT_FILE not set" >&2 exit 0 fi -EXECUTABLE_PATH=$1 -ARCH="386" - -if [[ $EXECUTABLE_PATH =~ "amd64" ]]; then - ARCH="amd64" +if [ ! -f "$CERT_FILE" ]; then + echo "error Windows code-signing; file '$CERT_FILE' not found" >&2 + exit 1 fi -OUT_PATH=gh_signed-${ARCH}.exe +if [ -z "$CERT_PASSWORD" ]; then + echo "error Windows code-signing; no value for CERT_PASSWORD" >&2 + exit 1 +fi -osslsigncode sign \ - -certs certificate.pem \ - -key private-key.pem \ - -n "GitHub CLI" \ - -t http://timestamp.digicert.com \ - -in $EXECUTABLE_PATH \ - -out $OUT_PATH +osslsigncode sign -n "GitHub CLI" -t http://timestamp.digicert.com \ + -pkcs12 "$CERT_FILE" -readpass <(printf "%s" "$CERT_PASSWORD") -h sha256 \ + -in "$EXE" -out "$EXE"~ -mv $OUT_PATH $EXECUTABLE_PATH +mv "$EXE"~ "$EXE" diff --git a/script/sign.ps1 b/script/sign.ps1 index ec724f7bd..336e0204c 100644 --- a/script/sign.ps1 +++ b/script/sign.ps1 @@ -6,12 +6,7 @@ param ( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" -$thumbprint = "fb713a60a7fa79dfc03cb301ca05d4e8c1bdd431" -$passwd = $env:GITHUB_CERT_PASSWORD $ProgramName = "GitHub CLI" - $scriptPath = split-path -parent $MyInvocation.MyCommand.Definition -& $scriptPath\signtool.exe sign /d $ProgramName /f $Certificate /p $passwd ` - /sha1 $thumbprint /fd sha256 /tr http://timestamp.digicert.com /td sha256 /v ` - $Executable +& $scriptPath\signtool.exe sign /d $ProgramName /f $Certificate /p $env:CERT_PASSWORD /fd sha256 /tr http://timestamp.digicert.com /v $Executable From 6e14426dfb076350796b6f081b27399a5f4be904 Mon Sep 17 00:00:00 2001 From: Breno Silva Date: Tue, 15 Feb 2022 13:35:30 -0300 Subject: [PATCH 07/83] docs: improve `repo edit` docs (#5165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mislav Marohnić --- pkg/cmd/repo/edit/edit.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/cmd/repo/edit/edit.go b/pkg/cmd/repo/edit/edit.go index 4761fb0b8..86ccdaab1 100644 --- a/pkg/cmd/repo/edit/edit.go +++ b/pkg/cmd/repo/edit/edit.go @@ -57,7 +57,19 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr - by URL, e.g. "https://github.com/OWNER/REPO" `), }, + 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 cmd.Flags().NFlag() == 0 { return cmdutil.FlagErrorf("at least one flag is required") From 72340076ae7baaf7c691c4b949a32639679d7df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 15 Feb 2022 18:27:58 +0100 Subject: [PATCH 08/83] tests --- pkg/cmd/release/create/create.go | 14 +- pkg/cmd/release/create/create_test.go | 245 +++++++++++++++++--------- pkg/cmd/release/create/http.go | 7 +- 3 files changed, 170 insertions(+), 96 deletions(-) diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index bdfdf1460..ce615ff70 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -364,13 +364,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, @@ -418,12 +411,7 @@ func createRun(opts *CreateOptions) error { } if !opts.Draft { - params := map[string]interface{}{} - params["draft"] = false - if opts.DiscussionCategory != "" { - params["discussion_category_name"] = opts.DiscussionCategory - } - rel, err := publishRelease(httpClient, newRelease.APIURL, params) + rel, err := publishRelease(httpClient, newRelease.APIURL, opts.DiscussionCategory) if err != nil { return err } diff --git a/pkg/cmd/release/create/create_test.go b/pkg/cmd/release/create/create_test.go index f13150002..8b3e13e18 100644 --- a/pkg/cmd/release/create/create_test.go +++ b/pkg/cmd/release/create/create_test.go @@ -286,7 +286,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 @@ -301,12 +301,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: ``, @@ -322,13 +330,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: ``, @@ -343,11 +359,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: ``, @@ -363,35 +387,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, @@ -403,11 +414,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: "", @@ -432,10 +451,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: ``, @@ -449,15 +555,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) { @@ -475,26 +576,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, ¶ms) - 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()) }) diff --git a/pkg/cmd/release/create/http.go b/pkg/cmd/release/create/http.go index b5ee5c271..628d06113 100644 --- a/pkg/cmd/release/create/http.go +++ b/pkg/cmd/release/create/http.go @@ -134,7 +134,12 @@ func createRelease(httpClient *http.Client, repo ghrepo.Interface, params map[st return &newRelease, err } -func publishRelease(httpClient *http.Client, releaseURL string, params map[string]interface{}) (*shared.Release, error) { +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 From c2a9c5a74b2aafc3ad33571a9b5f6b21dd2405cd Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Tue, 15 Feb 2022 13:36:06 -0500 Subject: [PATCH 09/83] Support filtering pull requests authored by GitHub Apps (#5180) --- pkg/cmd/pr/list/list.go | 14 +++++++++++ pkg/cmd/pr/list/list_test.go | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index acc4ffb14..319efa4f7 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -36,6 +36,7 @@ type ListOptions struct { HeadBranch string Labels []string Author string + AppAuthor string Assignee string Search string Draft string @@ -82,6 +83,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) } @@ -99,6 +108,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman cmd.Flags().StringVarP(&opts.HeadBranch, "head", "H", "", "Filter by head branch") cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by labels") 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 +173,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 diff --git a/pkg/cmd/pr/list/list_test.go b/pkg/cmd/pr/list/list_test.go index 6e340510c..a2db7eb04 100644 --- a/pkg/cmd/pr/list/list_test.go +++ b/pkg/cmd/pr/list/list_test.go @@ -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) From 0635514ddfab4d2fc4ca2dfb729b0b59a0fe97da Mon Sep 17 00:00:00 2001 From: Patrick Veverka Date: Tue, 15 Feb 2022 02:06:01 +0000 Subject: [PATCH 10/83] first stab wip move to method param flushing out --- internal/codespaces/api/api.go | 54 ++++++++++++++++++++++++++++ pkg/cmd/codespace/common.go | 1 + pkg/cmd/codespace/edit.go | 65 ++++++++++++++++++++++++++++++++++ pkg/cmd/codespace/mock_api.go | 55 ++++++++++++++++++++++++++++ pkg/cmd/codespace/root.go | 1 + 5 files changed, 176 insertions(+) create mode 100644 pkg/cmd/codespace/edit.go diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index a207f562c..9bd41db5d 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -686,6 +686,60 @@ func (a *API) DeleteCodespace(ctx context.Context, codespaceName string) error { return nil } +type EditCodespaceParams struct { + DisplayName string + IdleTimeoutMinutes int + Machine string +} + +type editRequest struct { + DisplayName string `json:"display_name,omitempty"` + IdleTimeoutMinutes int `json:"idle_timeout_minutes,omitempty"` + Machine string `json:"machine"` +} + +func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *EditCodespaceParams) (*Codespace, error) { + requestBody, err := json.Marshal(editRequest{ + DisplayName: params.DisplayName, + IdleTimeoutMinutes: params.IdleTimeoutMinutes, + Machine: params.Machine, + }) + fmt.Printf("requestBody: %s\n", requestBody) + 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.StatusAccepted { + return nil, errProvisioningInProgress // RPC finished before result of creation known + } else if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + 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 +} + type getCodespaceRepositoryContentsResponse struct { Content string `json:"content"` } diff --git a/pkg/cmd/codespace/common.go b/pkg/cmd/codespace/common.go index d7273edb8..bdc4d6ef4 100644 --- a/pkg/cmd/codespace/common.go +++ b/pkg/cmd/codespace/common.go @@ -62,6 +62,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) diff --git a/pkg/cmd/codespace/edit.go b/pkg/cmd/codespace/edit.go new file mode 100644 index 000000000..16b4d3ad2 --- /dev/null +++ b/pkg/cmd/codespace/edit.go @@ -0,0 +1,65 @@ +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") + 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) + } + + fmt.Fprintln(a.io.Out, codespace.Name) + return nil +} diff --git a/pkg/cmd/codespace/mock_api.go b/pkg/cmd/codespace/mock_api.go index fd275e802..220750afe 100644 --- a/pkg/cmd/codespace/mock_api.go +++ b/pkg/cmd/codespace/mock_api.go @@ -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 { diff --git a/pkg/cmd/codespace/root.go b/pkg/cmd/codespace/root.go index 5b2c0d8fc..0a04d2fdd 100644 --- a/pkg/cmd/codespace/root.go +++ b/pkg/cmd/codespace/root.go @@ -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)) From 4d45bc765495e48aeba8f23b07fad03ddf268bee Mon Sep 17 00:00:00 2001 From: Patrick Veverka Date: Tue, 15 Feb 2022 20:11:03 +0000 Subject: [PATCH 11/83] more changes --- internal/codespaces/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 9bd41db5d..fc94b22fd 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -695,7 +695,7 @@ type EditCodespaceParams struct { type editRequest struct { DisplayName string `json:"display_name,omitempty"` IdleTimeoutMinutes int `json:"idle_timeout_minutes,omitempty"` - Machine string `json:"machine"` + Machine string `json:"machine,omitempty"` } func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *EditCodespaceParams) (*Codespace, error) { From e3ff873d649ce3675a2dc5e7672f41165099b1e6 Mon Sep 17 00:00:00 2001 From: Patrick Veverka Date: Wed, 16 Feb 2022 00:00:13 +0000 Subject: [PATCH 12/83] since we can change the machine name, we should probably allow them to list it --- internal/codespaces/api/api.go | 20 ++++++++++++++++---- pkg/cmd/codespace/edit.go | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index fc94b22fd..1653e06ed 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -170,6 +170,7 @@ type Codespace struct { State string `json:"state"` GitStatus CodespaceGitStatus `json:"git_status"` Connection CodespaceConnection `json:"connection"` + Machine CodespaceMachine `json:"machine"` } type CodespaceGitStatus struct { @@ -180,6 +181,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 +217,7 @@ var CodespaceFields = []string{ "gitStatus", "createdAt", "lastUsedAt", + "machineName", } func (c *Codespace) ExportData(fields []string) map[string]interface{} { @@ -219,6 +230,8 @@ 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, @@ -265,6 +278,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) @@ -704,7 +718,7 @@ func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *E IdleTimeoutMinutes: params.IdleTimeoutMinutes, Machine: params.Machine, }) - fmt.Printf("requestBody: %s\n", requestBody) + if err != nil { return nil, fmt.Errorf("error marshaling request: %w", err) } @@ -721,9 +735,7 @@ func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *E } defer resp.Body.Close() - if resp.StatusCode == http.StatusAccepted { - return nil, errProvisioningInProgress // RPC finished before result of creation known - } else if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { return nil, api.HandleHTTPError(resp) } diff --git a/pkg/cmd/codespace/edit.go b/pkg/cmd/codespace/edit.go index 16b4d3ad2..6b1af1c8c 100644 --- a/pkg/cmd/codespace/edit.go +++ b/pkg/cmd/codespace/edit.go @@ -60,6 +60,6 @@ func (a *App) Edit(ctx context.Context, opts editOptions) error { return fmt.Errorf("error editing codespace: %w", err) } - fmt.Fprintln(a.io.Out, codespace.Name) + fmt.Fprintln(a.io.Out, codespace.DisplayName) return nil } From 590acaa1d666543b93a3c6adb81e50dcf722ac92 Mon Sep 17 00:00:00 2001 From: Mark Phelps Date: Tue, 15 Feb 2022 19:35:53 -0500 Subject: [PATCH 13/83] WIP --- internal/codespaces/api/api.go | 26 +++++++++++++++++++++++ pkg/cmd/codespace/code.go | 13 +++--------- pkg/cmd/codespace/code_test.go | 6 ++++-- pkg/cmd/codespace/common.go | 8 ++++++- pkg/cmd/codespace/create.go | 36 +++++++++++++++++++++++++++++++- pkg/cmd/codespace/delete_test.go | 2 +- pkg/cmd/root/root.go | 1 + 7 files changed, 77 insertions(+), 15 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index a207f562c..63f86c931 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -614,6 +614,15 @@ type startCreateRequest struct { 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 @@ -648,6 +657,23 @@ 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 + + b, err := ioutil.ReadAll(resp.Body) + 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 + } + + return nil, api.HandleHTTPError(resp) + } else if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { return nil, api.HandleHTTPError(resp) } diff --git a/pkg/cmd/codespace/code.go b/pkg/cmd/codespace/code.go index 2c66b4655..f80e32527 100644 --- a/pkg/cmd/codespace/code.go +++ b/pkg/cmd/codespace/code.go @@ -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,7 +30,7 @@ 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 { +func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool) error { if codespaceName == "" { codespace, err := chooseCodespace(ctx, a.apiClient) if err != nil { @@ -50,7 +43,7 @@ func (a *App) VSCode(ctx context.Context, browser browser, codespaceName string, } url := vscodeProtocolURL(codespaceName, useInsiders) - if err := browser.Browse(url); err != nil { + if err := a.browser.Browse(url); err != nil { return fmt.Errorf("error opening Visual Studio Code: %w", err) } diff --git a/pkg/cmd/codespace/code_test.go b/pkg/cmd/codespace/code_test.go index 956219bb2..cbc59864a 100644 --- a/pkg/cmd/codespace/code_test.go +++ b/pkg/cmd/codespace/code_test.go @@ -40,8 +40,10 @@ 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, + } + 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) diff --git a/pkg/cmd/codespace/common.go b/pkg/cmd/codespace/common.go index d7273edb8..d21f9ff6c 100644 --- a/pkg/cmd/codespace/common.go +++ b/pkg/cmd/codespace/common.go @@ -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, } } diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index ab04c2bae..97a292067 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -1,14 +1,17 @@ package codespace import ( + "bufio" "context" "errors" "fmt" + "io" "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/utils" "github.com/spf13/cobra" ) @@ -117,7 +120,32 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()), }) a.StopProgressIndicator() + + out := a.io.Out + if err != nil { + var aerr api.AcceptPermissionsRequiredError + if errors.As(err, &aerr) && aerr.AllowPermissionsURL != "" { + + var ( + isInteractive = a.io.CanPrompt() + cs = a.io.ColorScheme() + displayURL = utils.DisplayURL(aerr.AllowPermissionsURL) + ) + + if !isInteractive { + fmt.Fprintf(out, "%s to continue in your web browser: %s\n", cs.Bold("Open this URL"), displayURL) + return nil + } + + fmt.Fprintf(out, "You must accept the permissions requested by the repository before you can create a codespace.\n") + fmt.Fprintf(out, "%s to open %s in your browser... ", cs.Bold("Press Enter"), displayURL) + _ = waitForEnter(a.io.In) + + if err := a.browser.Browse(aerr.AllowPermissionsURL); err != nil { + return fmt.Errorf("error opening browser: %w", err) + } + } return fmt.Errorf("error creating codespace: %w", err) } @@ -127,7 +155,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { } } - fmt.Fprintln(a.io.Out, codespace.Name) + fmt.Fprintln(out, codespace.Name) return nil } @@ -296,3 +324,9 @@ func buildDisplayName(displayName string, prebuildAvailability string) string { return fmt.Sprintf("%s%s", displayName, prebuildText) } + +func waitForEnter(r io.Reader) error { + scanner := bufio.NewScanner(r) + scanner.Scan() + return scanner.Err() +} diff --git a/pkg/cmd/codespace/delete_test.go b/pkg/cmd/codespace/delete_test.go index 89318b5e3..638641d64 100644 --- a/pkg/cmd/codespace/delete_test.go +++ b/pkg/cmd/codespace/delete_test.go @@ -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) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index cff797e93..68ba157ff 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -142,6 +142,7 @@ func newCodespaceCmd(f *cmdutil.Factory) *cobra.Command { vscsURL, &lazyLoadedHTTPClient{factory: f}, ), + f.Browser, ) cmd := codespaceCmd.NewRootCmd(app) cmd.Use = "codespace" From 133be27d997bf2935143da11b0011f798f84538a Mon Sep 17 00:00:00 2001 From: Patrick Veverka Date: Wed, 16 Feb 2022 13:35:46 +0000 Subject: [PATCH 14/83] wip tests --- internal/codespaces/api/api_test.go | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/internal/codespaces/api/api_test.go b/internal/codespaces/api/api_test.go index af394cf7e..fe0663644 100644 --- a/internal/codespaces/api/api_test.go +++ b/internal/codespaces/api/api_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "reflect" "strconv" "testing" ) @@ -265,3 +266,48 @@ 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{} + }{ + // TODO: Add test cases. + } + 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) + } + }) + } +} From 3d093d1a95a1ace4d9af7dee016e43a72883a1a9 Mon Sep 17 00:00:00 2001 From: Patrick Veverka Date: Thu, 17 Feb 2022 02:23:17 +0000 Subject: [PATCH 15/83] some tests --- internal/codespaces/api/api_test.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/codespaces/api/api_test.go b/internal/codespaces/api/api_test.go index fe0663644..35616b42d 100644 --- a/internal/codespaces/api/api_test.go +++ b/internal/codespaces/api/api_test.go @@ -289,7 +289,18 @@ func TestCodespace_ExportData(t *testing.T) { args args want map[string]interface{} }{ - // TODO: Add test cases. + { + name: "just name", + fields: fields{ + Name: "test", + }, + args: args{ + fields: []string{"name"}, + }, + want: map[string]interface{}{ + "name": "test", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From c5dbf20ed45b00abef46ab330239688ac403452d Mon Sep 17 00:00:00 2001 From: Christian Gregg Date: Thu, 17 Feb 2022 12:53:42 +0000 Subject: [PATCH 16/83] Alias `list` as `ls` for all commands (#5214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I always get tripped up whenever trying to list my codespaces, adding `ls` as an alias to `list` feels natural enough. Co-authored-by: Mislav Marohnić --- pkg/cmd/alias/list/list.go | 5 +++-- pkg/cmd/codespace/list.go | 7 ++++--- pkg/cmd/config/list/list.go | 7 ++++--- pkg/cmd/extension/command.go | 7 ++++--- pkg/cmd/gist/list/list.go | 7 ++++--- pkg/cmd/gpg-key/list/list.go | 7 ++++--- pkg/cmd/issue/list/list.go | 3 ++- pkg/cmd/pr/list/list.go | 3 ++- pkg/cmd/release/list/list.go | 7 ++++--- pkg/cmd/repo/deploy-key/list/list.go | 7 ++++--- pkg/cmd/run/list/list.go | 7 ++++--- pkg/cmd/secret/list/list.go | 3 ++- pkg/cmd/ssh-key/list/list.go | 7 ++++--- pkg/cmd/workflow/list/list.go | 9 +++++---- 14 files changed, 50 insertions(+), 36 deletions(-) diff --git a/pkg/cmd/alias/list/list.go b/pkg/cmd/alias/list/list.go index 85609e7a4..973a4ebee 100644 --- a/pkg/cmd/alias/list/list.go +++ b/pkg/cmd/alias/list/list.go @@ -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. `), diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 7fe71d6fa..661d1da1c 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -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) diff --git a/pkg/cmd/config/list/list.go b/pkg/cmd/config/list/list.go index c2ad0397b..cf1422f0f 100644 --- a/pkg/cmd/config/list/list.go +++ b/pkg/cmd/config/list/list.go @@ -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) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index baec485d0..d4ab83ed7 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -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 { diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go index 38c55425d..4fe081a80 100644 --- a/pkg/cmd/gist/list/list.go +++ b/pkg/cmd/gist/list/list.go @@ -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) diff --git a/pkg/cmd/gpg-key/list/list.go b/pkg/cmd/gpg-key/list/list.go index 2ea047305..f52a6c48d 100644 --- a/pkg/cmd/gpg-key/list/list.go +++ b/pkg/cmd/gpg-key/list/list.go @@ -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) diff --git a/pkg/cmd/issue/list/list.go b/pkg/cmd/issue/list/list.go index cba60b75f..da9447a54 100644 --- a/pkg/cmd/issue/list/list.go +++ b/pkg/cmd/issue/list/list.go @@ -66,7 +66,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman $ 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 diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index 319efa4f7..01e71b948 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -70,7 +70,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Open the list of PRs in a web browser $ gh pr list --web `), - 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 diff --git a/pkg/cmd/release/list/list.go b/pkg/cmd/release/list/list.go index 18e33621d..f7e7d98ed 100644 --- a/pkg/cmd/release/list/list.go +++ b/pkg/cmd/release/list/list.go @@ -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 diff --git a/pkg/cmd/repo/deploy-key/list/list.go b/pkg/cmd/repo/deploy-key/list/list.go index 599c2e55e..f982f7f6e 100644 --- a/pkg/cmd/repo/deploy-key/list/list.go +++ b/pkg/cmd/repo/deploy-key/list/list.go @@ -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 diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index 8c1f59779..4a208b321 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -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 diff --git a/pkg/cmd/secret/list/list.go b/pkg/cmd/secret/list/list.go index 27fa941bf..e197ad748 100644 --- a/pkg/cmd/secret/list/list.go +++ b/pkg/cmd/secret/list/list.go @@ -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 diff --git a/pkg/cmd/ssh-key/list/list.go b/pkg/cmd/ssh-key/list/list.go index e743f2818..764737e09 100644 --- a/pkg/cmd/ssh-key/list/list.go +++ b/pkg/cmd/ssh-key/list/list.go @@ -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) diff --git a/pkg/cmd/workflow/list/list.go b/pkg/cmd/workflow/list/list.go index e39c3bfc0..69d994f2a 100644 --- a/pkg/cmd/workflow/list/list.go +++ b/pkg/cmd/workflow/list/list.go @@ -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 From f4b4a4e18f3ab25fde80c1319373e33ca9634f78 Mon Sep 17 00:00:00 2001 From: Hanjingxue Boling <87272716+Hanjingxue-Boling@users.noreply.github.com> Date: Fri, 18 Feb 2022 02:41:26 +0800 Subject: [PATCH 17/83] Update installation instructions for Fedora (#5204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gh-cli has entered fedora's official repository and can be installed directly without adding a third-party repository. Co-authored-by: Mislav Marohnić --- docs/install_linux.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/install_linux.md b/docs/install_linux.md index b07e2478b..2c3579d65 100644 --- a/docs/install_linux.md +++ b/docs/install_linux.md @@ -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 From 1eefda0b459a980873167bf0fb0a653f48acfc78 Mon Sep 17 00:00:00 2001 From: Jonathan Mayer <85513336+jonathan-enf@users.noreply.github.com> Date: Thu, 17 Feb 2022 11:35:59 -0800 Subject: [PATCH 18/83] gh auth login: add git protocol flag (#5158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mislav Marohnić --- pkg/cmd/auth/login/login.go | 17 +++++++----- pkg/cmd/auth/shared/login_flow.go | 5 ++-- pkg/cmdutil/flags.go | 44 +++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 51e74861d..ce52c55b9 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -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 { @@ -54,8 +55,8 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm Alternatively, pass in a token on standard input by using %[1]s--with-token%[1]s. 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. + The %[1]s--scopes%[1]s 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. `, "`"), Example: heredoc.Doc(` # start interactive setup @@ -64,7 +65,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 { @@ -111,6 +112,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes for gh to have") 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 +188,7 @@ func loginRun(opts *LoginOptions) error { Web: opts.Web, Scopes: opts.Scopes, Executable: opts.MainExecutable, + GitProtocol: opts.GitProtocol, }) } diff --git a/pkg/cmd/auth/shared/login_flow.go b/pkg/cmd/auth/shared/login_flow.go index 3570106da..c33051e44 100644 --- a/pkg/cmd/auth/shared/login_flow.go +++ b/pkg/cmd/auth/shared/login_flow.go @@ -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?", diff --git a/pkg/cmdutil/flags.go b/pkg/cmdutil/flags.go index 7472fa42c..7359d8c51 100644 --- a/pkg/cmdutil/flags.go +++ b/pkg/cmdutil/flags.go @@ -1,7 +1,9 @@ package cmdutil import ( + "fmt" "strconv" + "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -21,6 +23,20 @@ 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 { + 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 formatValuesForUsageDocs(values []string) string { + return fmt.Sprintf("{%s}", strings.Join(values, "|")) +} + type stringValue struct { string **string } @@ -75,3 +91,31 @@ 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 { + found := false + for _, opt := range e.options { + if strings.EqualFold(opt, value) { + found = true + break + } + } + if !found { + 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" +} From e7c2f973ae828d05cead21a5f9be3c4f3b9c8db6 Mon Sep 17 00:00:00 2001 From: Mark Phelps Date: Thu, 17 Feb 2022 15:11:12 -0500 Subject: [PATCH 19/83] Support opting out of permissions --- internal/codespaces/api/api.go | 3 ++ pkg/cmd/codespace/create.go | 92 ++++++++++++++++++++++++++-------- 2 files changed, 73 insertions(+), 22 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 63f86c931..aae1c08be 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -565,6 +565,7 @@ type CreateCodespaceParams struct { Branch string Machine string Location string + PermissionsOptOut bool } // CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it @@ -610,6 +611,7 @@ type startCreateRequest struct { Ref string `json:"ref"` Location string `json:"location"` Machine string `json:"machine"` + PermissionsOptOut bool `json:"devcontainer_permissions_opt_out"` } var errProvisioningInProgress = errors.New("provisioning in progress") @@ -638,6 +640,7 @@ func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (* Ref: params.Branch, Location: params.Location, Machine: params.Machine, + PermissionsOptOut: params.PermissionsOptOut, }) if err != nil { return nil, fmt.Errorf("error marshaling request: %w", err) diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index 97a292067..32fb4b77a 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -16,11 +16,12 @@ import ( ) 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 { @@ -38,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, "skip-permissions", "", false, "do not 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\"") @@ -118,35 +120,81 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { Machine: machine, Location: locationResult.Location, IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()), + PermissionsOptOut: opts.permissionsOptOut, }) a.StopProgressIndicator() - out := a.io.Out - if err != nil { var aerr api.AcceptPermissionsRequiredError - if errors.As(err, &aerr) && aerr.AllowPermissionsURL != "" { + if !errors.As(err, &aerr) || aerr.AllowPermissionsURL == "" { + return fmt.Errorf("error creating codespace: %w", err) + } - var ( - isInteractive = a.io.CanPrompt() - cs = a.io.ColorScheme() - displayURL = utils.DisplayURL(aerr.AllowPermissionsURL) - ) + var ( + isInteractive = a.io.CanPrompt() + cs = a.io.ColorScheme() + displayURL = utils.DisplayURL(aerr.AllowPermissionsURL) + ) - if !isInteractive { - fmt.Fprintf(out, "%s to continue in your web browser: %s\n", cs.Bold("Open this URL"), displayURL) - return nil - } + fmt.Fprintf(a.io.Out, "You must accept or deny additional permissions requested by the repository before you can create a codespace.\n") - fmt.Fprintf(out, "You must accept the permissions requested by the repository before you can create a codespace.\n") - fmt.Fprintf(out, "%s to open %s in your browser... ", cs.Bold("Press Enter"), displayURL) - _ = waitForEnter(a.io.In) + if !isInteractive { + fmt.Fprintf(a.io.Out, "%s to continue in your web browser to accept: %s\n", cs.Bold("Open this URL"), displayURL) + fmt.Fprintf(a.io.Out, "Alternatively, you can run %q with the %q option to create the codespace without these additional permissions.\n", a.io.ColorScheme().Bold("create"), cs.Bold("--skip-permissions")) + return nil + } + choices := []string{ + "Continue in browser to accept the additional permissions", + "Create the codespace without these 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 fmt.Errorf("error getting answers: %w", err) + } + + // if the user chose to continue in the browser, open the URL + if answers.Accept == choices[0] { if err := a.browser.Browse(aerr.AllowPermissionsURL); err != nil { return 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 + } + + // if the user chose to create the codespace without the permissions, + // we can continue with the create opting out of the additional permissions + a.StartProgressIndicatorWithLabel("Creating codespace") + codespace, err = a.apiClient.CreateCodespace(ctx, &api.CreateCodespaceParams{ + RepositoryID: repository.ID, + Branch: branch, + Machine: machine, + Location: locationResult.Location, + IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()), + PermissionsOptOut: true, + }) + a.StopProgressIndicator() + + if err != nil { + return fmt.Errorf("error creating codespace: %w", err) } - return fmt.Errorf("error creating codespace: %w", err) } if opts.showStatus { @@ -155,7 +203,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { } } - fmt.Fprintln(out, codespace.Name) + fmt.Fprintln(a.io.Out, codespace.Name) return nil } From 19563c4a742abf6671bb5028aa2cf91dcba40110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 18 Feb 2022 12:52:47 +0100 Subject: [PATCH 20/83] Use StringEnumFlag helper in more places --- pkg/cmd/completion/completion.go | 3 +-- pkg/cmd/completion/completion_test.go | 2 +- pkg/cmd/issue/list/list.go | 5 +---- pkg/cmd/pr/diff/diff.go | 4 ++-- pkg/cmd/pr/diff/diff_test.go | 2 +- pkg/cmd/pr/list/list.go | 5 +---- pkg/cmd/secret/set/set.go | 6 +----- pkg/cmdutil/flags.go | 1 + 8 files changed, 9 insertions(+), 19 deletions(-) diff --git a/pkg/cmd/completion/completion.go b/pkg/cmd/completion/completion.go index 496f06784..d83dd31ef 100644 --- a/pkg/cmd/completion/completion.go +++ b/pkg/cmd/completion/completion.go @@ -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 } diff --git a/pkg/cmd/completion/completion_test.go b/pkg/cmd/completion/completion_test.go index 06b3d2f27..a5068f747 100644 --- a/pkg/cmd/completion/completion_test.go +++ b/pkg/cmd/completion/completion_test.go @@ -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 { diff --git a/pkg/cmd/issue/list/list.go b/pkg/cmd/issue/list/list.go index da9447a54..94cd636cd 100644 --- a/pkg/cmd/issue/list/list.go +++ b/pkg/cmd/issue/list/list.go @@ -86,10 +86,7 @@ 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().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 - }) + 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") diff --git a/pkg/cmd/pr/diff/diff.go b/pkg/cmd/pr/diff/diff.go index 6e166ebb0..95fce5cd1 100644 --- a/pkg/cmd/pr/diff/diff.go +++ b/pkg/cmd/pr/diff/diff.go @@ -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 diff --git a/pkg/cmd/pr/diff/diff_test.go b/pkg/cmd/pr/diff/diff_test.go index f25b46309..5fbc0c236 100644 --- a/pkg/cmd/pr/diff/diff_test.go +++ b/pkg/cmd/pr/diff/diff_test.go @@ -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 { diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index 01e71b948..24d02d8fe 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -101,10 +101,7 @@ 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().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") diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index 33874d0a7..0c46d5f70 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -121,10 +121,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,7 +145,7 @@ 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") diff --git a/pkg/cmdutil/flags.go b/pkg/cmdutil/flags.go index 7359d8c51..dee70069e 100644 --- a/pkg/cmdutil/flags.go +++ b/pkg/cmdutil/flags.go @@ -25,6 +25,7 @@ func NilBoolFlag(cmd *cobra.Command, p **bool, name string, shorthand string, us // 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) { From 512208ca961d42caa2660e0d09dec30b468c42b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 18 Feb 2022 12:54:07 +0100 Subject: [PATCH 21/83] repo fork: tweak docs --- pkg/cmd/repo/fork/fork.go | 29 ++++++++++++++++------------- pkg/cmd/repo/fork/fork_test.go | 4 ++-- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index 62063f102..c3b559c97 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -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 [] [-- ...]", 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 } diff --git a/pkg/cmd/repo/fork/fork_test.go b/pkg/cmd/repo/fork/fork_test.go index 69e73a395..749bf7e61 100644 --- a/pkg/cmd/repo/fork/fork_test.go +++ b/pkg/cmd/repo/fork/fork_test.go @@ -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", From 57f49d9f8a7c1f0a98b31577d3a08367fcb6f89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 18 Feb 2022 17:12:25 +0100 Subject: [PATCH 22/83] issue/pr comment: improve docs as it relates to editor --- pkg/cmd/issue/comment/comment.go | 16 +++++++++++----- pkg/cmd/pr/comment/comment.go | 18 ++++++++---------- pkg/cmd/pr/shared/commentable.go | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/issue/comment/comment.go b/pkg/cmd/issue/comment/comment.go index 41c76eeea..a3f1b0b4d 100644 --- a/pkg/cmd/issue/comment/comment.go +++ b/pkg/cmd/issue/comment/comment.go @@ -23,9 +23,15 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*prShared.CommentableOptions) e cmd := &cobra.Command{ Use: "comment { | }", - 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 } diff --git a/pkg/cmd/pr/comment/comment.go b/pkg/cmd/pr/comment/comment.go index ff04c2813..1ad79fd11 100644 --- a/pkg/cmd/pr/comment/comment.go +++ b/pkg/cmd/pr/comment/comment.go @@ -22,17 +22,15 @@ func NewCmdComment(f *cmdutil.Factory, runF func(*shared.CommentableOptions) err cmd := &cobra.Command{ Use: "comment [ | | ]", - 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 } diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index cff10284a..2ddc770a6 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -65,7 +65,7 @@ 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 { From 0f1aaae57ce75dd14f50e9381cb778ddc1865291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 18 Feb 2022 17:12:43 +0100 Subject: [PATCH 23/83] issue/pr comment: allow using editor in non-interactive mode The user should be able to `gh config set prompt disabled` and still be allowed to use `gh issue comment --editor`. --- pkg/cmd/pr/shared/commentable.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index 2ddc770a6..788e86c5d 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -68,10 +68,6 @@ func CommentablePreRun(cmd *cobra.Command, opts *CommentableOptions) error { 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`") } From 5f8bdd64dadd47d764020da438856311aab1cdb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 18 Feb 2022 17:17:21 +0100 Subject: [PATCH 24/83] gh api: clarify how to pass API preview names --- pkg/cmd/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/api/api.go b/pkg/cmd/api/api.go index 4762ac77d..36f8eeb23 100644 --- a/pkg/cmd/api/api.go +++ b/pkg/cmd/api/api.go @@ -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)") From 286ba5e8dc628fd68b332736c4dfeb7c109c84f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 18 Feb 2022 17:32:44 +0100 Subject: [PATCH 25/83] issue/pr list: clarify search syntax --- pkg/cmd/issue/list/list.go | 21 +++++++++++++-------- pkg/cmd/pr/list/list.go | 23 +++++++++++++---------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/pkg/cmd/issue/list/list.go b/pkg/cmd/issue/list/list.go index da9447a54..f589c1ffb 100644 --- a/pkg/cmd/issue/list/list.go +++ b/pkg/cmd/issue/list/list.go @@ -57,12 +57,17 @@ 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: + + `), 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" `), @@ -83,9 +88,9 @@ 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().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by label") 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 @@ -93,7 +98,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman 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) diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index 01e71b948..9238f03d6 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -53,22 +53,25 @@ 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: + + `), 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 "" --state merged `), Aliases: []string{"ls"}, Args: cmdutil.NoArgsQuoteReminder, @@ -99,7 +102,7 @@ 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) { @@ -107,7 +110,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman }) 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") From 7235384f1278988c09037cba83f0a0f5389aff96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 18 Feb 2022 17:58:30 +0100 Subject: [PATCH 26/83] gh: list help topics in help output --- pkg/cmd/actions/actions.go | 10 ++++------ pkg/cmd/root/help.go | 23 ++++++++++++++++++++++- pkg/cmd/root/root.go | 3 --- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/actions/actions.go b/pkg/cmd/actions/actions.go index e5620c42e..d24df6631 100644 --- a/pkg/cmd/actions/actions.go +++ b/pkg/cmd/actions/actions.go @@ -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) diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go index d154df6a8..bc6b20665 100644 --- a/pkg/cmd/root/help.go +++ b/pkg/cmd/root/help.go @@ -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) diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index cff797e93..315b26501 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -50,9 +50,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. - `), }, } From 0f956c85025d20c6003328bfbe72159264218302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 18 Feb 2022 18:00:00 +0100 Subject: [PATCH 27/83] auth login: clarify authentication alternatives --- pkg/cmd/auth/login/login.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index ce52c55b9..e8353fd4a 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -50,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 %[1]s--scopes%[1]s 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 @@ -109,7 +113,7 @@ 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") From 9cbfe73140a7afe4814ade9a45b2d1dd4a58d6ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 18 Feb 2022 18:25:41 +0100 Subject: [PATCH 28/83] Have core commands have consistent Short description --- pkg/cmd/auth/auth.go | 5 ++++- pkg/cmd/release/release.go | 2 +- pkg/cmd/repo/repo.go | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/auth/auth.go b/pkg/cmd/auth/auth.go index e5ae67fa2..66757daf9 100644 --- a/pkg/cmd/auth/auth.go +++ b/pkg/cmd/auth/auth.go @@ -14,8 +14,11 @@ import ( func NewCmdAuth(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "auth ", - Short: "Login, logout, and refresh your authentication", + Short: "Authenticate gh with GitHub", Long: `Manage gh's authentication state.`, + Annotations: map[string]string{ + "IsCore": "true", + }, } cmdutil.DisableAuthCheck(cmd) diff --git a/pkg/cmd/release/release.go b/pkg/cmd/release/release.go index 2d68418f0..c5a132cd2 100644 --- a/pkg/cmd/release/release.go +++ b/pkg/cmd/release/release.go @@ -15,7 +15,7 @@ import ( func NewCmdRelease(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "release ", - Short: "Manage GitHub releases", + Short: "Manage releases", Annotations: map[string]string{ "IsCore": "true", }, diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index 08207c76e..e8696d7d9 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -22,7 +22,7 @@ import ( func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "repo ", - Short: "Create, clone, fork, and view repositories", + Short: "Manage repositories", Long: `Work with GitHub repositories.`, Example: heredoc.Doc(` $ gh repo create From 57d058150419c404c3cdd32ed6e746534bc3aa45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 21 Feb 2022 15:24:54 +0100 Subject: [PATCH 29/83] Add note about git auth --- pkg/cmd/auth/auth.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/cmd/auth/auth.go b/pkg/cmd/auth/auth.go index 66757daf9..c3df8486d 100644 --- a/pkg/cmd/auth/auth.go +++ b/pkg/cmd/auth/auth.go @@ -14,8 +14,7 @@ import ( func NewCmdAuth(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "auth ", - Short: "Authenticate gh with GitHub", - Long: `Manage gh's authentication state.`, + Short: "Authenticate gh and git with GitHub", Annotations: map[string]string{ "IsCore": "true", }, From 1ea26f33d8eeefaa8924c934ce771ae5e4ca896c Mon Sep 17 00:00:00 2001 From: Mark Phelps Date: Tue, 22 Feb 2022 09:50:43 -0500 Subject: [PATCH 30/83] Add create test --- pkg/cmd/codespace/create_test.go | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pkg/cmd/codespace/create_test.go b/pkg/cmd/codespace/create_test.go index 4bf4671ea..795552284 100644 --- a/pkg/cmd/codespace/create_test.go +++ b/pkg/cmd/codespace/create_test.go @@ -69,6 +69,56 @@ 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, + }, + wantStdout: `You must accept or deny additional permissions requested by the repository before you can create a codespace. +Open this URL to continue in your web browser to accept: example.com/permissions +Alternatively, you can run "create" with the "--skip-permissions" option to create the codespace without these additional permissions. +`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 707220a9b1f045d1f76447b1d18de0c73d10ec83 Mon Sep 17 00:00:00 2001 From: Mark Phelps Date: Tue, 22 Feb 2022 14:16:03 -0500 Subject: [PATCH 31/83] PR updates --- internal/codespaces/api/api.go | 10 ++- pkg/cmd/codespace/create.go | 114 +++++++++++++++++---------------- 2 files changed, 66 insertions(+), 58 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index aae1c08be..14da851ae 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -661,9 +661,13 @@ 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 + var ( + ue AcceptPermissionsRequiredError + bodyCopy = &bytes.Buffer{} + r = io.TeeReader(resp.Body, bodyCopy) + ) - b, err := ioutil.ReadAll(resp.Body) + b, err := ioutil.ReadAll(r) if err != nil { return nil, fmt.Errorf("error reading response body: %w", err) } @@ -675,6 +679,8 @@ func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (* return nil, ue } + resp.Body = ioutil.NopCloser(bodyCopy) + return nil, api.HandleHTTPError(resp) } else if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index 32fb4b77a..20952bb6d 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -1,16 +1,15 @@ package codespace import ( - "bufio" "context" "errors" "fmt" - "io" "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" ) @@ -39,7 +38,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, "skip-permissions", "", false, "do not accept additional permissions requested by the codespace") + 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\"") @@ -130,53 +129,9 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { return fmt.Errorf("error creating codespace: %w", err) } - var ( - isInteractive = a.io.CanPrompt() - cs = a.io.ColorScheme() - displayURL = utils.DisplayURL(aerr.AllowPermissionsURL) - ) - - fmt.Fprintf(a.io.Out, "You must accept or deny additional permissions requested by the repository before you can create a codespace.\n") - - if !isInteractive { - fmt.Fprintf(a.io.Out, "%s to continue in your web browser to accept: %s\n", cs.Bold("Open this URL"), displayURL) - fmt.Fprintf(a.io.Out, "Alternatively, you can run %q with the %q option to create the codespace without these additional permissions.\n", a.io.ColorScheme().Bold("create"), cs.Bold("--skip-permissions")) - return nil - } - - choices := []string{ - "Continue in browser to accept the additional permissions", - "Create the codespace without these 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 fmt.Errorf("error getting answers: %w", err) - } - - // if the user chose to continue in the browser, open the URL - if answers.Accept == choices[0] { - if err := a.browser.Browse(aerr.AllowPermissionsURL); err != nil { - return 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 + if err := a.handleAdditionalPermissions(ctx, aerr.AllowPermissionsURL); 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 the user chose to create the codespace without the permissions, @@ -207,6 +162,59 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { return nil } +func (a *App) handleAdditionalPermissions(ctx context.Context, allowPermissionsURL string) error { + var ( + isInteractive = a.io.CanPrompt() + cs = a.io.ColorScheme() + displayURL = utils.DisplayURL(allowPermissionsURL) + ) + + fmt.Fprintf(a.io.ErrOut, "You must accept or deny additional permissions requested by the repository before you can create a codespace.\n") + + if !isInteractive { + fmt.Fprintf(a.io.ErrOut, "%s to continue in your web browser to accept: %s\n", cs.Bold("Open this URL"), displayURL) + fmt.Fprintf(a.io.ErrOut, "Alternatively, you can run %q with the %q option to create the codespace without these additional permissions.\n", a.io.ColorScheme().Bold("create"), cs.Bold("--default-permissions")) + return nil + } + + choices := []string{ + "Continue in browser to accept the additional permissions", + "Create the codespace without these 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 fmt.Errorf("error getting answers: %w", err) + } + + // if the user chose to continue in the browser, open the URL + if answers.Accept == choices[0] { + if err := a.browser.Browse(allowPermissionsURL); err != nil { + return 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 cmdutil.SilentError + } + + return 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. @@ -372,9 +380,3 @@ func buildDisplayName(displayName string, prebuildAvailability string) string { return fmt.Sprintf("%s%s", displayName, prebuildText) } - -func waitForEnter(r io.Reader) error { - scanner := bufio.NewScanner(r) - scanner.Scan() - return scanner.Err() -} From 71f32376d09d1d8330c72dec2e15982797232bce Mon Sep 17 00:00:00 2001 From: Mark Phelps Date: Tue, 22 Feb 2022 20:20:52 -0500 Subject: [PATCH 32/83] Fix tests --- pkg/cmd/codespace/create.go | 52 +++++++++++++++----------------- pkg/cmd/codespace/create_test.go | 10 +++--- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index 20952bb6d..3f2ee0c35 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -112,15 +112,17 @@ 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, IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()), PermissionsOptOut: opts.permissionsOptOut, - }) + } + + a.StartProgressIndicatorWithLabel("Creating codespace") + codespace, err := a.apiClient.CreateCodespace(ctx, createParams) a.StopProgressIndicator() if err != nil { @@ -129,27 +131,11 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { return fmt.Errorf("error creating codespace: %w", err) } - if err := a.handleAdditionalPermissions(ctx, aerr.AllowPermissionsURL); err != nil { + 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 the user chose to create the codespace without the permissions, - // we can continue with the create opting out of the additional permissions - a.StartProgressIndicatorWithLabel("Creating codespace") - codespace, err = a.apiClient.CreateCodespace(ctx, &api.CreateCodespaceParams{ - RepositoryID: repository.ID, - Branch: branch, - Machine: machine, - Location: locationResult.Location, - IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()), - PermissionsOptOut: true, - }) - a.StopProgressIndicator() - - if err != nil { - return fmt.Errorf("error creating codespace: %w", err) - } } if opts.showStatus { @@ -162,7 +148,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { return nil } -func (a *App) handleAdditionalPermissions(ctx context.Context, allowPermissionsURL string) error { +func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api.CreateCodespaceParams, allowPermissionsURL string) (*api.Codespace, error) { var ( isInteractive = a.io.CanPrompt() cs = a.io.ColorScheme() @@ -174,7 +160,7 @@ func (a *App) handleAdditionalPermissions(ctx context.Context, allowPermissionsU if !isInteractive { fmt.Fprintf(a.io.ErrOut, "%s to continue in your web browser to accept: %s\n", cs.Bold("Open this URL"), displayURL) fmt.Fprintf(a.io.ErrOut, "Alternatively, you can run %q with the %q option to create the codespace without these additional permissions.\n", a.io.ColorScheme().Bold("create"), cs.Bold("--default-permissions")) - return nil + return nil, cmdutil.SilentError } choices := []string{ @@ -199,20 +185,32 @@ func (a *App) handleAdditionalPermissions(ctx context.Context, allowPermissionsU } if err := ask(permsSurvey, &answers); err != nil { - return fmt.Errorf("error getting answers: %w", err) + 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] { if err := a.browser.Browse(allowPermissionsURL); err != nil { - return fmt.Errorf("error opening browser: %w", err) + 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 cmdutil.SilentError + return nil, cmdutil.SilentError } - return nil + // 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 diff --git a/pkg/cmd/codespace/create_test.go b/pkg/cmd/codespace/create_test.go index 795552284..376f8c9eb 100644 --- a/pkg/cmd/codespace/create_test.go +++ b/pkg/cmd/codespace/create_test.go @@ -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 }{ @@ -114,9 +115,10 @@ func TestApp_Create(t *testing.T) { showStatus: false, idleTimeout: 30 * time.Minute, }, - wantStdout: `You must accept or deny additional permissions requested by the repository before you can create a codespace. + wantErr: cmdutil.SilentError, + wantStderr: `You must accept or deny additional permissions requested by the repository before you can create a codespace. Open this URL to continue in your web browser to accept: example.com/permissions -Alternatively, you can run "create" with the "--skip-permissions" option to create the codespace without these additional permissions. +Alternatively, you can run "create" with the "--default-permissions" option to create the codespace without these additional permissions. `, }, } @@ -127,7 +129,7 @@ Alternatively, you can run "create" with the "--skip-permissions" option to crea 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 { From 86feaff6a9f44be7dd83411fd4b970e8b15b211c Mon Sep 17 00:00:00 2001 From: Patrick Veverka Date: Thu, 24 Feb 2022 20:21:06 +0000 Subject: [PATCH 33/83] little bit of testing --- internal/codespaces/api/api_test.go | 112 ++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/internal/codespaces/api/api_test.go b/internal/codespaces/api/api_test.go index 35616b42d..ebbbe5209 100644 --- a/internal/codespaces/api/api_test.go +++ b/internal/codespaces/api/api_test.go @@ -301,6 +301,34 @@ func TestCodespace_ExportData(t *testing.T) { "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) { @@ -322,3 +350,87 @@ func TestCodespace_ExportData(t *testing.T) { }) } } + +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) + } + }) + } +} From dd5feda00a2357275fb2cc2d1b4eb854e882faed Mon Sep 17 00:00:00 2001 From: Mark Phelps Date: Fri, 25 Feb 2022 17:32:08 -0500 Subject: [PATCH 34/83] Update wording after consulting product --- pkg/cmd/codespace/create.go | 10 +++++----- pkg/cmd/codespace/create_test.go | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index 3f2ee0c35..b145a6a7d 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -155,17 +155,17 @@ func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api displayURL = utils.DisplayURL(allowPermissionsURL) ) - fmt.Fprintf(a.io.ErrOut, "You must accept or deny additional permissions requested by the repository before you can create a codespace.\n") + 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 to continue in your web browser to accept: %s\n", cs.Bold("Open this URL"), displayURL) - fmt.Fprintf(a.io.ErrOut, "Alternatively, you can run %q with the %q option to create the codespace without these additional permissions.\n", a.io.ColorScheme().Bold("create"), cs.Bold("--default-permissions")) + 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 accept the additional permissions", - "Create the codespace without these additional permissions", + "Continue in browser to review and authorize additional permissions", + "Continue without authorizing additional permissions", } permsSurvey := []*survey.Question{ diff --git a/pkg/cmd/codespace/create_test.go b/pkg/cmd/codespace/create_test.go index 376f8c9eb..2d417620d 100644 --- a/pkg/cmd/codespace/create_test.go +++ b/pkg/cmd/codespace/create_test.go @@ -116,9 +116,9 @@ func TestApp_Create(t *testing.T) { idleTimeout: 30 * time.Minute, }, wantErr: cmdutil.SilentError, - wantStderr: `You must accept or deny additional permissions requested by the repository before you can create a codespace. -Open this URL to continue in your web browser to accept: example.com/permissions -Alternatively, you can run "create" with the "--default-permissions" option to create the codespace without these additional permissions. + 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. `, }, } From a6161cb8cb972ed754f0413bf202dfa465fbbd73 Mon Sep 17 00:00:00 2001 From: boonhong Date: Sat, 26 Feb 2022 18:03:05 +0800 Subject: [PATCH 35/83] pr close skips deleting local branch if not in a git repo --- pkg/cmd/pr/close/close.go | 44 ++++++++++++++++++---------------- pkg/cmd/pr/close/close_test.go | 34 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/pkg/cmd/pr/close/close.go b/pkg/cmd/pr/close/close.go index fc954b19e..c596509a6 100644 --- a/pkg/cmd/pr/close/close.go +++ b/pkg/cmd/pr/close/close.go @@ -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()) } } diff --git a/pkg/cmd/pr/close/close_test.go b/pkg/cmd/pr/close/close_test.go index a91431243..f0fdc7354 100644 --- a/pkg/cmd/pr/close/close_test.go +++ b/pkg/cmd/pr/close/close_test.go @@ -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()) +} From 25a4877f2e0e06fb059f990f6f63963222d9c507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 28 Feb 2022 09:31:27 +0100 Subject: [PATCH 36/83] Update triage.md (#5238) --- docs/triage.md | 51 +++++++++++++++----------------------------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/docs/triage.md b/docs/triage.md index caad1e52c..8a329de7c 100644 --- a/docs/triage.md +++ b/docs/triage.md @@ -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 From 12f2e3cab026e0187c9f1152baa77c89d9ba2399 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 14:27:35 +0000 Subject: [PATCH 37/83] Bump github.com/itchyny/gojq from 0.12.6 to 0.12.7 (#5263) Bumps [github.com/itchyny/gojq](https://github.com/itchyny/gojq) from 0.12.6 to 0.12.7. - [Release notes](https://github.com/itchyny/gojq/releases) - [Changelog](https://github.com/itchyny/gojq/blob/main/CHANGELOG.md) - [Commits](https://github.com/itchyny/gojq/compare/v0.12.6...v0.12.7) --- updated-dependencies: - dependency-name: github.com/itchyny/gojq dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 0dc6c68dc..59cd5006d 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -38,7 +38,7 @@ require ( github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20211205182925-97ca703d548d + golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 golang.org/x/term v0.0.0-20210503060354-a79de5458b56 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/go.sum b/go.sum index 94c81b705..b91f9192a 100644 --- a/go.sum +++ b/go.sum @@ -277,8 +277,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: 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= @@ -619,8 +619,9 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc 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-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= From a9e92c9ed5b7fda5b1a2d2f84c37932de804e1a9 Mon Sep 17 00:00:00 2001 From: Mark Phelps Date: Tue, 1 Mar 2022 09:37:10 -0500 Subject: [PATCH 38/83] Fix HexToRGB panic (#5247) --- pkg/iostreams/color.go | 2 +- pkg/iostreams/color_test.go | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go index 9ef9d7ac4..73fb5aa3c 100644 --- a/pkg/iostreams/color.go +++ b/pkg/iostreams/color.go @@ -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 } diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go index 3434b1de7..b34c7a97f 100644 --- a/pkg/iostreams/color_test.go +++ b/pkg/iostreams/color_test.go @@ -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 { From 97ea5e77b4d61d5d80ed08f7512847dee3ec9af5 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Tue, 1 Mar 2022 08:37:58 -0600 Subject: [PATCH 39/83] Fix obtained scope mapping to include implied scopes (#5256) * Fix obtained scope mapping to include missing scopes As is, `gotScopes` does not contain certain scopes because it doesn't check for the "special" scopes that imply other scopes. For example, the `repo` scope implies `repo:invite`. * Add a comment in ScopesSuggestion explaining branch statements * Delete whitespace to appease go-fmt --- api/client.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/api/client.go b/api/client.go index 3c95f6250..bfd526fbc 100644 --- a/api/client.go +++ b/api/client.go @@ -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:") { From edfaac23dac6ba49e66444872b74c2cf3083af79 Mon Sep 17 00:00:00 2001 From: Patrick Veverka Date: Wed, 2 Mar 2022 13:17:57 +0000 Subject: [PATCH 40/83] updates from review --- internal/codespaces/api/api.go | 14 ++------------ pkg/cmd/codespace/edit.go | 3 +-- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 1653e06ed..9aa19da2e 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -701,23 +701,13 @@ func (a *API) DeleteCodespace(ctx context.Context, codespaceName string) error { } type EditCodespaceParams struct { - DisplayName string - IdleTimeoutMinutes int - Machine string -} - -type editRequest 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(editRequest{ - DisplayName: params.DisplayName, - IdleTimeoutMinutes: params.IdleTimeoutMinutes, - Machine: params.Machine, - }) + requestBody, err := json.Marshal(params) if err != nil { return nil, fmt.Errorf("error marshaling request: %w", err) @@ -735,7 +725,7 @@ func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *E } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + if resp.StatusCode != http.StatusOK { return nil, api.HandleHTTPError(resp) } diff --git a/pkg/cmd/codespace/edit.go b/pkg/cmd/codespace/edit.go index 6b1af1c8c..70935d0eb 100644 --- a/pkg/cmd/codespace/edit.go +++ b/pkg/cmd/codespace/edit.go @@ -50,7 +50,7 @@ func (a *App) Edit(ctx context.Context, opts editOptions) error { SKU: opts.machine, } a.StartProgressIndicatorWithLabel("Editing codespace") - codespace, err := a.apiClient.EditCodespace(ctx, userInputs.CodespaceName, &api.EditCodespaceParams{ + _, err := a.apiClient.EditCodespace(ctx, userInputs.CodespaceName, &api.EditCodespaceParams{ DisplayName: userInputs.DisplayName, IdleTimeoutMinutes: int(userInputs.IdleTimeout.Minutes()), Machine: userInputs.SKU, @@ -60,6 +60,5 @@ func (a *App) Edit(ctx context.Context, opts editOptions) error { return fmt.Errorf("error editing codespace: %w", err) } - fmt.Fprintln(a.io.Out, codespace.DisplayName) return nil } From 0cce0e154344fe31e50ac6375f67212593c1b53b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 14:29:49 +0000 Subject: [PATCH 41/83] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- .github/workflows/go.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/releases.yml | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d46c0bcf3..720f1210e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Initialize CodeQL uses: github/codeql-action/init@v1 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f12d8f0d0..f3f5502ea 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -15,7 +15,7 @@ jobs: go-version: 1.16 - name: Check out code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Cache Go modules uses: actions/cache@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 78d94c78e..90e2c67e0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,7 +22,7 @@ jobs: go-version: 1.16 - name: Check out code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Verify dependencies run: | diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 6511fc71c..3e7f6cf2f 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -10,7 +10,7 @@ jobs: 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: @@ -44,7 +44,7 @@ jobs: GORELEASER_CURRENT_TAG: ${{steps.changelog.outputs.tag-name}} 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 @@ -128,7 +128,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 @@ -188,7 +188,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 From c38ca830be5b34dad8a457076ac61fa203e33d0d Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Thu, 3 Mar 2022 11:10:22 -0800 Subject: [PATCH 42/83] Extract shared.GetJob so we can use in rerun too --- pkg/cmd/run/shared/shared.go | 12 ++++++++++++ pkg/cmd/run/view/view.go | 14 +------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/cmd/run/shared/shared.go b/pkg/cmd/run/shared/shared.go index b015ecc0e..81cbcc8f4 100644 --- a/pkg/cmd/run/shared/shared.go +++ b/pkg/cmd/run/shared/shared.go @@ -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() diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index f41f37801..63518a632 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -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 { From bf5801e646221e93784ae1db76a75989088b154a Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Thu, 3 Mar 2022 11:49:08 -0800 Subject: [PATCH 43/83] Implement `--job` for rerunning a specific actions job --- pkg/cmd/run/rerun/rerun.go | 112 ++++++++++++++++++++++++-------- pkg/cmd/run/rerun/rerun_test.go | 45 ++++++++++++- 2 files changed, 129 insertions(+), 28 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 98cfa7060..21a3c31d1 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -20,6 +20,7 @@ type RerunOptions struct { RunID string OnlyFailed bool + JobID string Prompt bool } @@ -38,12 +39,22 @@ 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 or job ID 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 != "" { + opts.RunID = "" + if opts.IO.CanPrompt() { + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.ErrOut, "%s both run and job IDs specified; ignoring run ID\n", cs.WarningIcon()) + } } if runF != nil { @@ -54,6 +65,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm } cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs") + cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "Rerun a specific job from a run, including dependencies") return cmd } @@ -70,10 +82,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 @@ -94,40 +119,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) + if opts.JobID != "" { + err = rerunJob(client, repo, selectedJob) + if err != nil { + return err + } + if opts.IO.CanPrompt() { + 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) + } + + err = rerunRun(client, repo, run, opts.OnlyFailed) + if err != nil { + return err + } + if opts.IO.CanPrompt() { + 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 opts.OnlyFailed { + 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) + 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("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) } - - if opts.IO.CanPrompt() { - cs := opts.IO.ColorScheme() - 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 } diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index 3fcf68ff3..094425290 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -67,6 +67,33 @@ func TestNewCmdRerun(t *testing.T) { OnlyFailed: true, }, }, + { + name: "with arg job", + tty: true, + cli: "--job 1234", + wants: RerunOptions{ + JobID: "1234", + }, + }, + { + name: "with args job and runID ignores runID", + tty: true, + cli: "1234 --job 5678", + wants: RerunOptions{ + JobID: "5678", + }, + }, + { + 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 { @@ -151,6 +178,22 @@ func TestRerun(t *testing.T) { }, 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, @@ -209,7 +252,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", }, } From 625f3ac14482e510865bd32a0eecc23fe8dafb86 Mon Sep 17 00:00:00 2001 From: Mark Phelps Date: Thu, 3 Mar 2022 15:36:53 -0500 Subject: [PATCH 44/83] Updates wording for codespaces accept permissions flow --- pkg/cmd/codespace/create.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index b145a6a7d..406ad10f6 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -164,7 +164,7 @@ func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api } choices := []string{ - "Continue in browser to review and authorize additional permissions", + "Continue in browser to review and authorize additional permissions (Recommended)", "Continue without authorizing additional permissions", } @@ -190,6 +190,7 @@ func (a *App) handleAdditionalPermissions(ctx context.Context, createParams *api // 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) } From 6f66a1d265d1b4b1e3d2e742decfd474932bc168 Mon Sep 17 00:00:00 2001 From: Seito Tanaka Date: Mon, 7 Mar 2022 16:14:26 +0900 Subject: [PATCH 45/83] Add version to `extension list` command (#5219) * Add version to extension list command * Trim sha only when displaying --- pkg/cmd/extension/command.go | 5 +++++ pkg/cmd/extension/command_test.go | 2 +- pkg/cmd/extension/extension.go | 4 ++++ pkg/extensions/extension.go | 1 + pkg/extensions/extension_mock.go | 36 +++++++++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/extension/command.go b/pkg/cmd/extension/command.go index d4ab83ed7..1c929482f 100644 --- a/pkg/cmd/extension/command.go +++ b/pkg/cmd/extension/command.go @@ -61,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" diff --git a/pkg/cmd/extension/command_test.go b/pkg/cmd/extension/command_test.go index 8f896eab0..7e5520fa4 100644 --- a/pkg/cmd/extension/command_test.go +++ b/pkg/cmd/extension/command_test.go @@ -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", diff --git a/pkg/cmd/extension/extension.go b/pkg/cmd/extension/extension.go index b4106228f..d1b814c57 100644 --- a/pkg/cmd/extension/extension.go +++ b/pkg/cmd/extension/extension.go @@ -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 == "" || diff --git a/pkg/extensions/extension.go b/pkg/extensions/extension.go index 5826f4cd6..a9e120dc9 100644 --- a/pkg/extensions/extension.go +++ b/pkg/extensions/extension.go @@ -20,6 +20,7 @@ type Extension interface { Path() string // Path to executable URL() string IsLocal() bool + CurrentVersion() string UpdateAvailable() bool IsBinary() bool } diff --git a/pkg/extensions/extension_mock.go b/pkg/extensions/extension_mock.go index b999ab955..5b418c3f1 100644 --- a/pkg/extensions/extension_mock.go +++ b/pkg/extensions/extension_mock.go @@ -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 { From a657fa808adab50f928c8caa126474b5940ad9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Tue, 8 Mar 2022 16:44:41 +0100 Subject: [PATCH 46/83] pr checks: fix error message when no checks were found Fixes the error message: no checks reported on the '' branch Now the correct branch name is reported. --- pkg/cmd/pr/checks/checks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/checks/checks.go b/pkg/cmd/pr/checks/checks.go index 37602e1e0..f9ae516c9 100644 --- a/pkg/cmd/pr/checks/checks.go +++ b/pkg/cmd/pr/checks/checks.go @@ -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 { From e0045f26b9e28ca2769115c54d627c4f2fd89420 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Wed, 9 Mar 2022 14:24:27 +0200 Subject: [PATCH 47/83] Add top level search command and search repos sub command (#5172) --- pkg/cmd/root/root.go | 2 + pkg/cmd/run/list/list_test.go | 22 +-- pkg/cmd/search/repos/repos.go | 203 ++++++++++++++++++++ pkg/cmd/search/repos/repos_test.go | 295 +++++++++++++++++++++++++++++ pkg/cmd/search/search.go | 20 ++ pkg/cmdutil/flags.go | 55 +++++- pkg/httpmock/stub.go | 19 ++ pkg/search/query.go | 111 +++++++++++ pkg/search/query_test.go | 135 +++++++++++++ pkg/search/result.go | 120 ++++++++++++ pkg/search/result_test.go | 46 +++++ pkg/search/searcher.go | 184 ++++++++++++++++++ pkg/search/searcher_mock.go | 116 ++++++++++++ pkg/search/searcher_test.go | 198 +++++++++++++++++++ pkg/text/convert.go | 29 +++ pkg/text/convert_test.go | 61 ++++++ 16 files changed, 1588 insertions(+), 28 deletions(-) create mode 100644 pkg/cmd/search/repos/repos.go create mode 100644 pkg/cmd/search/repos/repos_test.go create mode 100644 pkg/cmd/search/search.go create mode 100644 pkg/search/query.go create mode 100644 pkg/search/query_test.go create mode 100644 pkg/search/result.go create mode 100644 pkg/search/result_test.go create mode 100644 pkg/search/searcher.go create mode 100644 pkg/search/searcher_mock.go create mode 100644 pkg/search/searcher_test.go create mode 100644 pkg/text/convert.go create mode 100644 pkg/text/convert_test.go diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 6ea865fe3..f77d17b55 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -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" @@ -79,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)) diff --git a/pkg/cmd/run/list/list_test.go b/pkg/cmd/run/list/list_test.go index fb18d04d2..a3d2b06de 100644 --- a/pkg/cmd/run/list/list_test.go +++ b/pkg/cmd/run/list/list_test.go @@ -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{}), diff --git a/pkg/cmd/search/repos/repos.go b/pkg/cmd/search/repos/repos.go new file mode 100644 index 000000000..878599dc6 --- /dev/null +++ b/pkg/cmd/search/repos/repos.go @@ -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 []", + 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 +} diff --git a/pkg/cmd/search/repos/repos_test.go b/pkg/cmd/search/repos/repos_test.go new file mode 100644 index 000000000..872171b01 --- /dev/null +++ b/pkg/cmd/search/repos/repos_test.go @@ -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()) + }) + } +} diff --git a/pkg/cmd/search/search.go b/pkg/cmd/search/search.go new file mode 100644 index 000000000..5342175c8 --- /dev/null +++ b/pkg/cmd/search/search.go @@ -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 ", + Short: "Search for repositories, issues, pull requests and users", + Long: "Search across all of GitHub.", + } + + cmd.AddCommand(searchReposCmd.NewCmdRepos(f, nil)) + + return cmd +} diff --git a/pkg/cmdutil/flags.go b/pkg/cmdutil/flags.go index dee70069e..2a73a79de 100644 --- a/pkg/cmdutil/flags.go +++ b/pkg/cmdutil/flags.go @@ -34,6 +34,16 @@ func StringEnumFlag(cmd *cobra.Command, p *string, name, shorthand, defaultValue 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, "|")) } @@ -99,14 +109,7 @@ type enumValue struct { } func (e *enumValue) Set(value string) error { - found := false - for _, opt := range e.options { - if strings.EqualFold(opt, value) { - found = true - break - } - } - if !found { + if !isIncluded(value, e.options) { return fmt.Errorf("valid values are %s", formatValuesForUsageDocs(e.options)) } *e.string = value @@ -120,3 +123,39 @@ func (e *enumValue) String() 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 +} diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index 5633c2caf..8970e8f49 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -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) diff --git a/pkg/search/query.go b/pkg/search/query.go new file mode 100644 index 000000000..a0966ba93 --- /dev/null +++ b/pkg/search/query.go @@ -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 +} diff --git a/pkg/search/query_test.go b/pkg/search/query_test.go new file mode 100644 index 000000000..889acae76 --- /dev/null +++ b/pkg/search/query_test.go @@ -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()) + }) + } +} diff --git a/pkg/search/result.go b/pkg/search/result.go new file mode 100644 index 000000000..99b3d2142 --- /dev/null +++ b/pkg/search/result.go @@ -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) + }) +} diff --git a/pkg/search/result_test.go b/pkg/search/result_test.go new file mode 100644 index 000000000..185cc4e36 --- /dev/null +++ b/pkg/search/result_test.go @@ -0,0 +1,46 @@ +package search + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepositoryExportData(t *testing.T) { + var createdAt = time.Date(2021, 2, 28, 12, 30, 0, 0, time.UTC) + tests := []struct { + name string + fields []string + repo Repository + output string + }{ + { + name: "exports requested fields", + fields: []string{"createdAt", "description", "fullName", "isArchived", "isFork", "isPrivate", "pushedAt"}, + repo: Repository{ + CreatedAt: createdAt, + Description: "description", + FullName: "cli/cli", + IsArchived: true, + IsFork: false, + IsPrivate: false, + PushedAt: createdAt, + }, + output: `{"createdAt":"2021-02-28T12:30:00Z","description":"description","fullName":"cli/cli","isArchived":true,"isFork":false,"isPrivate":false,"pushedAt":"2021-02-28T12:30:00Z"}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exported := tt.repo.ExportData(tt.fields) + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + require.NoError(t, enc.Encode(exported)) + assert.Equal(t, tt.output, strings.TrimSpace(buf.String())) + }) + } +} diff --git a/pkg/search/searcher.go b/pkg/search/searcher.go new file mode 100644 index 000000000..39d2c09c5 --- /dev/null +++ b/pkg/search/searcher.go @@ -0,0 +1,184 @@ +package search + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/cli/cli/v2/internal/ghinstance" +) + +const ( + maxPerPage = 100 + orderKey = "order" + sortKey = "sort" +) + +var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`) +var pageRE = regexp.MustCompile(`(\?|&)page=(\d*)`) +var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`) + +//go:generate moq -rm -out searcher_mock.go . Searcher +type Searcher interface { + Repositories(Query) (RepositoriesResult, error) + URL(Query) string +} + +type searcher struct { + client *http.Client + host string +} + +type httpError struct { + Errors []httpErrorItem + Message string + RequestURL *url.URL + StatusCode int +} + +type httpErrorItem struct { + Code string + Field string + Message string + Resource string +} + +func NewSearcher(client *http.Client, host string) Searcher { + return &searcher{ + client: client, + host: host, + } +} + +func (s searcher) Repositories(query Query) (RepositoriesResult, error) { + result := RepositoriesResult{} + toRetrieve := query.Limit + var resp *http.Response + var err error + for toRetrieve > 0 { + query.Limit = min(toRetrieve, maxPerPage) + query.Page = nextPage(resp) + if query.Page == 0 { + break + } + page := RepositoriesResult{} + resp, err = s.search(query, &page) + if err != nil { + return result, err + } + result.IncompleteResults = page.IncompleteResults + result.Total = page.Total + result.Items = append(result.Items, page.Items...) + toRetrieve = toRetrieve - len(page.Items) + } + return result, nil +} + +func (s searcher) search(query Query, result interface{}) (*http.Response, error) { + path := fmt.Sprintf("%ssearch/%s", ghinstance.RESTPrefix(s.host), query.Kind) + qs := url.Values{} + qs.Set("page", strconv.Itoa(query.Page)) + qs.Set("per_page", strconv.Itoa(query.Limit)) + qs.Set("q", query.String()) + if query.Order != "" { + qs.Set(orderKey, query.Order) + } + if query.Sort != "" { + qs.Set(sortKey, query.Sort) + } + url := fmt.Sprintf("%s?%s", path, qs.Encode()) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Accept", "application/vnd.github.v3+json") + resp, err := s.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + success := resp.StatusCode >= 200 && resp.StatusCode < 300 + if !success { + return resp, handleHTTPError(resp) + } + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(result) + if err != nil { + return resp, err + } + return resp, nil +} + +func (s searcher) URL(query Query) string { + path := fmt.Sprintf("https://%s/search", s.host) + qs := url.Values{} + qs.Set("type", query.Kind) + qs.Set("q", query.String()) + if query.Order != "" { + qs.Set(orderKey, query.Order) + } + if query.Sort != "" { + qs.Set(sortKey, query.Sort) + } + url := fmt.Sprintf("%s?%s", path, qs.Encode()) + return url +} + +func (err httpError) Error() string { + if err.StatusCode != 422 || len(err.Errors) == 0 { + return fmt.Sprintf("HTTP %d: %s (%s)", err.StatusCode, err.Message, err.RequestURL) + } + query := strings.TrimSpace(err.RequestURL.Query().Get("q")) + return fmt.Sprintf("Invalid search query %q.\n%s", query, err.Errors[0].Message) +} + +func handleHTTPError(resp *http.Response) error { + httpError := httpError{ + RequestURL: resp.Request.URL, + StatusCode: resp.StatusCode, + } + if !jsonTypeRE.MatchString(resp.Header.Get("Content-Type")) { + httpError.Message = resp.Status + return httpError + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + if err := json.Unmarshal(body, &httpError); err != nil { + return err + } + return httpError +} + +func nextPage(resp *http.Response) (page int) { + if resp == nil { + return 1 + } + for _, m := range linkRE.FindAllStringSubmatch(resp.Header.Get("Link"), -1) { + if !(len(m) > 2 && m[2] == "next") { + continue + } + p := pageRE.FindStringSubmatch(m[1]) + if len(p) == 3 { + i, err := strconv.Atoi(p[2]) + if err == nil { + return i + } + } + } + return 0 +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/pkg/search/searcher_mock.go b/pkg/search/searcher_mock.go new file mode 100644 index 000000000..9d584867b --- /dev/null +++ b/pkg/search/searcher_mock.go @@ -0,0 +1,116 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package search + +import ( + "sync" +) + +// Ensure, that SearcherMock does implement Searcher. +// If this is not the case, regenerate this file with moq. +var _ Searcher = &SearcherMock{} + +// SearcherMock is a mock implementation of Searcher. +// +// func TestSomethingThatUsesSearcher(t *testing.T) { +// +// // make and configure a mocked Searcher +// mockedSearcher := &SearcherMock{ +// RepositoriesFunc: func(query Query) (RepositoriesResult, error) { +// panic("mock out the Repositories method") +// }, +// URLFunc: func(query Query) string { +// panic("mock out the URL method") +// }, +// } +// +// // use mockedSearcher in code that requires Searcher +// // and then make assertions. +// +// } +type SearcherMock struct { + // RepositoriesFunc mocks the Repositories method. + RepositoriesFunc func(query Query) (RepositoriesResult, error) + + // URLFunc mocks the URL method. + URLFunc func(query Query) string + + // calls tracks calls to the methods. + calls struct { + // Repositories holds details about calls to the Repositories method. + Repositories []struct { + // Query is the query argument value. + Query Query + } + // URL holds details about calls to the URL method. + URL []struct { + // Query is the query argument value. + Query Query + } + } + lockRepositories sync.RWMutex + lockURL sync.RWMutex +} + +// Repositories calls RepositoriesFunc. +func (mock *SearcherMock) Repositories(query Query) (RepositoriesResult, error) { + if mock.RepositoriesFunc == nil { + panic("SearcherMock.RepositoriesFunc: method is nil but Searcher.Repositories was just called") + } + callInfo := struct { + Query Query + }{ + Query: query, + } + mock.lockRepositories.Lock() + mock.calls.Repositories = append(mock.calls.Repositories, callInfo) + mock.lockRepositories.Unlock() + return mock.RepositoriesFunc(query) +} + +// RepositoriesCalls gets all the calls that were made to Repositories. +// Check the length with: +// len(mockedSearcher.RepositoriesCalls()) +func (mock *SearcherMock) RepositoriesCalls() []struct { + Query Query +} { + var calls []struct { + Query Query + } + mock.lockRepositories.RLock() + calls = mock.calls.Repositories + mock.lockRepositories.RUnlock() + return calls +} + +// URL calls URLFunc. +func (mock *SearcherMock) URL(query Query) string { + if mock.URLFunc == nil { + panic("SearcherMock.URLFunc: method is nil but Searcher.URL was just called") + } + callInfo := struct { + Query Query + }{ + Query: query, + } + mock.lockURL.Lock() + mock.calls.URL = append(mock.calls.URL, callInfo) + mock.lockURL.Unlock() + return mock.URLFunc(query) +} + +// URLCalls gets all the calls that were made to URL. +// Check the length with: +// len(mockedSearcher.URLCalls()) +func (mock *SearcherMock) URLCalls() []struct { + Query Query +} { + var calls []struct { + Query Query + } + mock.lockURL.RLock() + calls = mock.calls.URL + mock.lockURL.RUnlock() + return calls +} diff --git a/pkg/search/searcher_test.go b/pkg/search/searcher_test.go new file mode 100644 index 000000000..9aadc32f1 --- /dev/null +++ b/pkg/search/searcher_test.go @@ -0,0 +1,198 @@ +package search + +import ( + "net/http" + "net/url" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/assert" +) + +var query = Query{ + Keywords: []string{"keyword"}, + Kind: "repositories", + Limit: 30, + Order: "stars", + Sort: "desc", + Qualifiers: Qualifiers{ + Stars: ">=5", + Topic: []string{"topic"}, + }, +} + +func TestSearcherRepositories(t *testing.T) { + values := url.Values{ + "page": []string{"1"}, + "per_page": []string{"30"}, + "order": []string{"stars"}, + "sort": []string{"desc"}, + "q": []string{"keyword stars:>=5 topic:topic"}, + } + + tests := []struct { + name string + host string + query Query + result RepositoriesResult + wantErr bool + errMsg string + httpStubs func(*httpmock.Registry) + }{ + { + name: "searches repositories", + query: query, + result: RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}}, + Total: 1, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.QueryMatcher("GET", "search/repositories", values), + httpmock.JSONResponse(RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}}, + Total: 1, + }), + ) + }, + }, + { + name: "searches repositories for enterprise host", + host: "enterprise.com", + query: query, + result: RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}}, + Total: 1, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.QueryMatcher("GET", "api/v3/search/repositories", values), + httpmock.JSONResponse(RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}}, + Total: 1, + }), + ) + }, + }, + { + name: "paginates results", + query: query, + result: RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}, {Name: "cli"}}, + Total: 2, + }, + httpStubs: func(reg *httpmock.Registry) { + firstReq := httpmock.QueryMatcher("GET", "search/repositories", values) + firstRes := httpmock.JSONResponse(RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "test"}}, + Total: 2, + }, + ) + firstRes = httpmock.WithHeader(firstRes, "Link", `; rel="next"`) + secondReq := httpmock.QueryMatcher("GET", "search/repositories", url.Values{ + "page": []string{"2"}, + "per_page": []string{"29"}, + "order": []string{"stars"}, + "sort": []string{"desc"}, + "q": []string{"keyword stars:>=5 topic:topic"}, + }, + ) + secondRes := httpmock.JSONResponse(RepositoriesResult{ + IncompleteResults: false, + Items: []Repository{{Name: "cli"}}, + Total: 2, + }, + ) + reg.Register(firstReq, firstRes) + reg.Register(secondReq, secondRes) + }, + }, + { + name: "handles search errors", + query: query, + wantErr: true, + errMsg: heredoc.Doc(` + Invalid search query "keyword stars:>=5 topic:topic". + "blah" is not a recognized date/time format. Please provide an ISO 8601 date/time value, such as YYYY-MM-DD.`), + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.QueryMatcher("GET", "search/repositories", values), + httpmock.WithHeader( + httpmock.StatusStringResponse(422, + `{ + "message":"Validation Failed", + "errors":[ + { + "message":"\"blah\" is not a recognized date/time format. Please provide an ISO 8601 date/time value, such as YYYY-MM-DD.", + "resource":"Search", + "field":"q", + "code":"invalid" + } + ], + "documentation_url":"https://docs.github.com/v3/search/" + }`, + ), "Content-Type", "application/json"), + ) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + if tt.httpStubs != nil { + tt.httpStubs(reg) + } + client := &http.Client{Transport: reg} + if tt.host == "" { + tt.host = "github.com" + } + searcher := NewSearcher(client, tt.host) + result, err := searcher.Repositories(tt.query) + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.result, result) + }) + } +} + +func TestSearcherURL(t *testing.T) { + tests := []struct { + name string + host string + query Query + url string + }{ + { + name: "outputs encoded query url", + query: query, + url: "https://github.com/search?order=stars&q=keyword+stars%3A%3E%3D5+topic%3Atopic&sort=desc&type=repositories", + }, + { + name: "supports enterprise hosts", + host: "enterprise.com", + query: query, + url: "https://enterprise.com/search?order=stars&q=keyword+stars%3A%3E%3D5+topic%3Atopic&sort=desc&type=repositories", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.host == "" { + tt.host = "github.com" + } + searcher := NewSearcher(nil, tt.host) + assert.Equal(t, tt.url, searcher.URL(tt.query)) + }) + } +} diff --git a/pkg/text/convert.go b/pkg/text/convert.go new file mode 100644 index 000000000..c5d2f401d --- /dev/null +++ b/pkg/text/convert.go @@ -0,0 +1,29 @@ +package text + +import "unicode" + +// Copied from: https://github.com/asaskevich/govalidator +func CamelToKebab(str string) string { + var output []rune + var segment []rune + for _, r := range str { + if !unicode.IsLower(r) && string(r) != "-" && !unicode.IsNumber(r) { + output = addSegment(output, segment) + segment = nil + } + segment = append(segment, unicode.ToLower(r)) + } + output = addSegment(output, segment) + return string(output) +} + +func addSegment(inrune, segment []rune) []rune { + if len(segment) == 0 { + return inrune + } + if len(inrune) != 0 { + inrune = append(inrune, '-') + } + inrune = append(inrune, segment...) + return inrune +} diff --git a/pkg/text/convert_test.go b/pkg/text/convert_test.go new file mode 100644 index 000000000..5321fbf10 --- /dev/null +++ b/pkg/text/convert_test.go @@ -0,0 +1,61 @@ +package text + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCamelToKebab(t *testing.T) { + tests := []struct { + name string + in string + out string + }{ + { + name: "single lowercase word", + in: "test", + out: "test", + }, + { + name: "multiple mixed words", + in: "testTestTest", + out: "test-test-test", + }, + { + name: "multiple uppercase words", + in: "TestTest", + out: "test-test", + }, + { + name: "multiple lowercase words", + in: "testtest", + out: "testtest", + }, + { + name: "multiple mixed words with number", + in: "test2Test", + out: "test2-test", + }, + { + name: "multiple lowercase words with number", + in: "test2test", + out: "test2test", + }, + { + name: "multiple lowercase words with dash", + in: "test-test", + out: "test-test", + }, + { + name: "multiple uppercase words with dash", + in: "Test-Test", + out: "test--test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.out, CamelToKebab(tt.in)) + }) + } +} From 4d5ce7aa564eb9780939ecec6a69fc1d3665d5bd Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Wed, 9 Mar 2022 10:59:29 -0500 Subject: [PATCH 48/83] Adds internal codespace developer flags (#5287) --- .devcontainer/devcontainer.json | 5 +++++ internal/codespaces/api/api.go | 7 +++++++ pkg/cmd/codespace/create.go | 24 +++++++++++++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..eac1bad83 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,5 @@ +{ + "extensions": [ + "golang.go" + ] +} \ No newline at end of file diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index afed04404..ec3a1ff66 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -579,6 +579,8 @@ type CreateCodespaceParams struct { Branch string Machine string Location string + VSCSTarget string + VSCSTargetURL string PermissionsOptOut bool } @@ -625,6 +627,8 @@ 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"` } @@ -654,8 +658,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) } diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go index b145a6a7d..26156a886 100644 --- a/pkg/cmd/codespace/create.go +++ b/pkg/cmd/codespace/create.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "time" "github.com/AlecAivazis/survey/v2" @@ -47,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 @@ -117,6 +124,8 @@ func (a *App) Create(ctx context.Context, opts createOptions) error { Branch: branch, Machine: machine, Location: locationResult.Location, + VSCSTarget: vscsTarget, + VSCSTargetURL: vscsTargetUrl, IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()), PermissionsOptOut: opts.permissionsOptOut, } @@ -282,9 +291,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} From ee1328de49876583df8ee04ff19c1ce5d3844e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 10 Mar 2022 17:28:39 +0100 Subject: [PATCH 49/83] Bump Cobra to eliminate Viper --- go.mod | 5 +- go.sum | 394 +++------------------------------------------------------ 2 files changed, 19 insertions(+), 380 deletions(-) diff --git a/go.mod b/go.mod index 59cd5006d..cd63fd72b 100644 --- a/go.mod +++ b/go.mod @@ -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 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-20220227234510-4e6760a101f9 - golang.org/x/term v0.0.0-20210503060354-a79de5458b56 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) diff --git a/go.sum b/go.sum index b91f9192a..f51a48221 100644 --- a/go.sum +++ b/go.sum @@ -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,69 +130,29 @@ 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.7 h1:hYPTpeWfrJ1OT+2j6cvBScbhl0TkdwGM4bc66onUSOQ= @@ -283,23 +161,13 @@ github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921i 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,59 @@ 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/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 +261,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 +269,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 +294,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 +315,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,47 +344,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/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= @@ -651,7 +381,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= @@ -676,22 +405,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= @@ -713,30 +429,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= @@ -760,46 +459,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= @@ -812,22 +477,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= @@ -837,24 +486,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= From cb7315c85d3c0e010ba117ca7e692ed6f18f16c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 10 Mar 2022 18:29:42 +0100 Subject: [PATCH 50/83] Re-initialize modules cache in CI --- .github/workflows/go.yml | 9 ++++----- .github/workflows/lint.yml | 8 ++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f3f5502ea..0d7e68d95 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -17,14 +17,13 @@ jobs: - name: Check out code 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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90e2c67e0..ea7d4ea34 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,6 +24,14 @@ jobs: - name: Check out code 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 From be5923770b3436f26df35c7c228353d5728a93a5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 16:42:31 -0600 Subject: [PATCH 51/83] Add vscs-target to `gh cs list` --- pkg/cmd/codespace/list.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 661d1da1c..6f6c902a1 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -43,6 +43,13 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er return fmt.Errorf("error getting codespaces: %w", err) } + hasNonDefaultVSCSTarget := false + for _, apiCodespace := range codespaces { + if apiCodespace.VSCSTarget != "prod" { + hasNonDefaultVSCSTarget = true + } + } + if err := a.io.StartPager(); err != nil { a.errLogger.Printf("error starting pager: %v", err) } @@ -59,6 +66,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 hasNonDefaultVSCSTarget { + tp.AddField("VSCS TARGET", nil, nil) + } + tp.EndRow() } @@ -88,6 +100,11 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er } else { tp.AddField(c.CreatedAt, nil, nil) } + + if hasNonDefaultVSCSTarget { + tp.AddField(c.VSCSTarget, nil, nil) + } + tp.EndRow() } From d85feafa85b90c1783c4fe1411a2d992e9826ea0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 17:05:13 -0600 Subject: [PATCH 52/83] Rename to hasNonProdVSCSTarget --- pkg/cmd/codespace/list.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 6f6c902a1..2ee890670 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -43,10 +43,10 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er return fmt.Errorf("error getting codespaces: %w", err) } - hasNonDefaultVSCSTarget := false + hasNonProdVSCSTarget := false for _, apiCodespace := range codespaces { if apiCodespace.VSCSTarget != "prod" { - hasNonDefaultVSCSTarget = true + hasNonProdVSCSTarget = true } } @@ -67,7 +67,7 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er tp.AddField("STATE", nil, nil) tp.AddField("CREATED AT", nil, nil) - if hasNonDefaultVSCSTarget { + if hasNonProdVSCSTarget { tp.AddField("VSCS TARGET", nil, nil) } @@ -101,7 +101,7 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er tp.AddField(c.CreatedAt, nil, nil) } - if hasNonDefaultVSCSTarget { + if hasNonProdVSCSTarget { tp.AddField(c.VSCSTarget, nil, nil) } From a3b69e8d879ef4e961ac2f96490dbc2f9aa9f510 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 17:10:36 -0600 Subject: [PATCH 53/83] Add missed file --- internal/codespaces/api/api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index ec3a1ff66..2e0a2153f 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -171,6 +171,7 @@ type Codespace struct { GitStatus CodespaceGitStatus `json:"git_status"` Connection CodespaceConnection `json:"connection"` Machine CodespaceMachine `json:"machine"` + VSCSTarget string `json:"vscs_target,omitempty"` } type CodespaceGitStatus struct { From 5e62a417d8c89d9d0cdc858298f1942661c58959 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 18:50:12 -0600 Subject: [PATCH 54/83] Add emojis to name --- internal/codespaces/api/api.go | 2 +- pkg/cmd/codespace/list.go | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 2e0a2153f..2a859f3ac 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -171,7 +171,7 @@ type Codespace struct { GitStatus CodespaceGitStatus `json:"git_status"` Connection CodespaceConnection `json:"connection"` Machine CodespaceMachine `json:"machine"` - VSCSTarget string `json:"vscs_target,omitempty"` + VSCSTarget string `json:"vscs_target"` } type CodespaceGitStatus struct { diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 2ee890670..987ed00af 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -86,7 +86,9 @@ 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) + + tp.AddField(formattedName, nil, cs.Yellow) tp.AddField(c.Repository.FullName, nil, nil) tp.AddField(c.branchWithGitStatus(), nil, cs.Cyan) tp.AddField(c.State, nil, stateColor) @@ -110,3 +112,15 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er return tp.Render() } + +func formatNameForVSCSTarget(name, vscsTarget string) string { + if vscsTarget == "dev" || vscsTarget == "local" { + return fmt.Sprintf("%s 🚧", name) + } + + if vscsTarget == "ppe" { + return fmt.Sprintf("%s ✨", name) + } + + return name +} From 21a1059f7cf75c6b0e96ce323ef25c6675663029 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 19:01:55 -0600 Subject: [PATCH 55/83] Add `--profile` option to `gh cs cp` --- pkg/cmd/codespace/ssh.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 726f2152f..6ce783f2f 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -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 } From 94128d683c621a2d73a59bfddad371ff7d2bf3d3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 19:59:24 -0600 Subject: [PATCH 56/83] Add consts for targets and treat empty as prod --- internal/codespaces/api/api.go | 7 +++++++ pkg/cmd/codespace/list.go | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 2a859f3ac..790ad0ee4 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -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 diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 987ed00af..17c7b6414 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -45,8 +45,9 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er hasNonProdVSCSTarget := false for _, apiCodespace := range codespaces { - if apiCodespace.VSCSTarget != "prod" { + if apiCodespace.VSCSTarget != "" && apiCodespace.VSCSTarget != api.VSCSTargetProduction { hasNonProdVSCSTarget = true + break } } @@ -114,11 +115,11 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er } func formatNameForVSCSTarget(name, vscsTarget string) string { - if vscsTarget == "dev" || vscsTarget == "local" { + if vscsTarget == api.VSCSTargetDevelopment || vscsTarget == api.VSCSTargetLocal { return fmt.Sprintf("%s 🚧", name) } - if vscsTarget == "ppe" { + if vscsTarget == api.VSCSTargetPPE { return fmt.Sprintf("%s ✨", name) } From 9278f51aa89795cd56f0c4e71f0ffe74c6dc5fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 11 Mar 2022 14:58:15 +0100 Subject: [PATCH 57/83] Bump golangci-lint version --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea7d4ea34..9d5522ad7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -37,7 +37,7 @@ jobs: 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/ From de88d9e186bc690a44b620e8fb2c4ed80319680f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 11 Mar 2022 11:13:38 -0600 Subject: [PATCH 58/83] Only export vscsTarget for non-prod --- internal/codespaces/api/api.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 790ad0ee4..d8807ec74 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -226,6 +226,7 @@ var CodespaceFields = []string{ "createdAt", "lastUsedAt", "machineName", + "vscsTarget", } func (c *Codespace) ExportData(fields []string) map[string]interface{} { @@ -246,6 +247,10 @@ func (c *Codespace) ExportData(fields []string) map[string]interface{} { "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) From bb9bf298353e62cc83a89d472ae4ca8b38c2ff28 Mon Sep 17 00:00:00 2001 From: Boston Cartwright Date: Mon, 14 Mar 2022 06:02:57 -0600 Subject: [PATCH 59/83] pr merge switch to base branch if available (#5251) * after merge, switch to base branch if available * Add ability to checkout new branch * Style cleanup Co-authored-by: Sam Coe --- git/git.go | 9 +++ pkg/cmd/pr/merge/merge.go | 45 +++++++------- pkg/cmd/pr/merge/merge_test.go | 103 +++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 24 deletions(-) diff --git a/git/git.go b/git/git.go index 7a3a81437..31fea6233 100644 --- a/git/git.go +++ b/git/git.go @@ -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) diff --git a/pkg/cmd/pr/merge/merge.go b/pkg/cmd/pr/merge/merge.go index e13d08b7a..f6d5fb37b 100644 --- a/pkg/cmd/pr/merge/merge.go +++ b/pkg/cmd/pr/merge/merge.go @@ -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 diff --git a/pkg/cmd/pr/merge/merge_test.go b/pkg/cmd/pr/merge/merge_test.go index d692b0a08..c7f30f7c4 100644 --- a/pkg/cmd/pr/merge/merge_test.go +++ b/pkg/cmd/pr/merge/merge_test.go @@ -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, "") From 07e0e52edd52bb46a5874767ca3b4fac50caa7c2 Mon Sep 17 00:00:00 2001 From: neilnaveen <42328488+neilnaveen@users.noreply.github.com> Date: Mon, 14 Mar 2022 08:18:21 -0500 Subject: [PATCH 60/83] Fixed permission for workflow (#5279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions Co-authored-by: Mislav Marohnić --- .github/workflows/codeql.yml | 5 +++++ .github/workflows/go.yml | 4 ++++ .github/workflows/issueauto.yml | 13 +++++++++---- .github/workflows/lint.yml | 3 +++ .github/workflows/prauto.yml | 6 ++++++ .github/workflows/releases.yml | 4 ++++ 6 files changed, 31 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 720f1210e..034253caf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -10,6 +10,11 @@ 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 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0d7e68d95..4fbf3d3b9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,5 +1,9 @@ name: Tests on: [push, pull_request] + +permissions: + contents: read + jobs: build: strategy: diff --git a/.github/workflows/issueauto.yml b/.github/workflows/issueauto.yml index a366d6ed8..40c4b36e7 100644 --- a/.github/workflows/issueauto.yml +++ b/.github/workflows/issueauto.yml @@ -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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea7d4ea34..e811ef6ef 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,6 +11,9 @@ on: - go.mod - go.sum +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest diff --git a/.github/workflows/prauto.yml b/.github/workflows/prauto.yml index 047fb52ea..2596fa76c 100644 --- a/.github/workflows/prauto.yml +++ b/.github/workflows/prauto.yml @@ -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 diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 3e7f6cf2f..5e570350a 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -5,6 +5,10 @@ on: tags: - "v*" +permissions: + contents: write # publishing releases + repository-projects: write # move cards between columns + jobs: goreleaser: runs-on: ubuntu-latest From 3645975da763411b0c0d9b7482be5bed774b6509 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Mon, 14 Mar 2022 10:00:29 -0700 Subject: [PATCH 61/83] Prefer IsStdoutTTY when that's all we need --- pkg/cmd/run/rerun/rerun.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 21a3c31d1..e382fd134 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -124,7 +124,7 @@ func runRerun(opts *RerunOptions) error { if err != nil { return err } - if opts.IO.CanPrompt() { + 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), @@ -142,7 +142,7 @@ func runRerun(opts *RerunOptions) error { if err != nil { return err } - if opts.IO.CanPrompt() { + if opts.IO.IsStdoutTTY() { onlyFailedMsg := "" if opts.OnlyFailed { onlyFailedMsg = "(failed jobs) " From 24ec53365bdf505116485189ec432bcc19adfca0 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Mon, 14 Mar 2022 10:01:19 -0700 Subject: [PATCH 62/83] Return error if both jobID and runID are specified --- pkg/cmd/run/rerun/rerun.go | 6 +----- pkg/cmd/run/rerun/rerun_test.go | 10 ++++------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index e382fd134..0303060b4 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -50,11 +50,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm } if opts.RunID != "" && opts.JobID != "" { - opts.RunID = "" - if opts.IO.CanPrompt() { - cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.ErrOut, "%s both run and job IDs specified; ignoring run ID\n", cs.WarningIcon()) - } + return cmdutil.FlagErrorf("specify only one of or ") } if runF != nil { diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index 094425290..b7bfc1d62 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -76,12 +76,10 @@ func TestNewCmdRerun(t *testing.T) { }, }, { - name: "with args job and runID ignores runID", - tty: true, - cli: "1234 --job 5678", - wants: RerunOptions{ - JobID: "5678", - }, + name: "with args jobID and runID fails", + tty: true, + cli: "1234 --job 5678", + wantsErr: true, }, { name: "with arg job with no ID fails", From 83a6cccf2ae36e4180aa0d6ab5a18b52311870db Mon Sep 17 00:00:00 2001 From: Gowtham Munukutla Date: Mon, 14 Mar 2022 23:19:03 +0530 Subject: [PATCH 63/83] Add interactive repository edit functionality (#4895) --- api/queries_repo.go | 3 + pkg/cmd/repo/edit/edit.go | 339 ++++++++++++++++++++++++++++++--- pkg/cmd/repo/edit/edit_test.go | 192 ++++++++++++++++++- 3 files changed, 499 insertions(+), 35 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index b7ea14915..a287b6557 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -46,6 +46,7 @@ type Repository struct { MergeCommitAllowed bool SquashMergeAllowed bool RebaseMergeAllowed bool + AutoMergeAllowed bool ForkCount int StargazerCount int @@ -68,6 +69,7 @@ type Repository struct { IsArchived bool IsEmpty bool IsFork bool + ForkingAllowed bool IsInOrganization bool IsMirror bool IsPrivate bool @@ -81,6 +83,7 @@ type Repository struct { ViewerPermission string ViewerPossibleCommitEmails []string ViewerSubscription string + Visibility string RepositoryTopics struct { Nodes []struct { diff --git a/pkg/cmd/repo/edit/edit.go b/pkg/cmd/repo/edit/edit.go index 86ccdaab1..1793caad2 100644 --- a/pkg/cmd/repo/edit/edit.go +++ b/pkg/cmd/repo/edit/edit.go @@ -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 []", @@ -71,10 +100,6 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(options *EditOptions) error) *cobr gh repo edit --enable-projects=false `), RunE: func(cmd *cobra.Command, args []string) error { - if cmd.Flags().NFlag() == 0 { - return cmdutil.FlagErrorf("at least one flag is required") - } - if len(args) > 0 { var err error opts.Repository, err = ghrepo.FromFullName(args[0]) @@ -95,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) } @@ -124,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{} @@ -144,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) @@ -164,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) { @@ -228,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 +} diff --git a/pkg/cmd/repo/edit/edit_test.go b/pkg/cmd/repo/edit/edit_test.go index 9130fbd64..1cf8fd4dd 100644 --- a/pkg/cmd/repo/edit/edit_test.go +++ b/pkg/cmd/repo/edit/edit_test.go @@ -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 == "" { From e6b09b45deb10c3d465b598e5ff91b4d3b22f2a4 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Mon, 14 Mar 2022 12:29:49 -0700 Subject: [PATCH 64/83] Fix up error and help language --- pkg/cmd/run/rerun/rerun.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/run/rerun/rerun.go b/pkg/cmd/run/rerun/rerun.go index 0303060b4..45f231f90 100644 --- a/pkg/cmd/run/rerun/rerun.go +++ b/pkg/cmd/run/rerun/rerun.go @@ -41,7 +41,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm if len(args) == 0 && opts.JobID == "" { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("run or job ID required when not running interactively") + return cmdutil.FlagErrorf("`` or `--job` required when not running interactively") } else { opts.Prompt = true } @@ -50,7 +50,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm } if opts.RunID != "" && opts.JobID != "" { - return cmdutil.FlagErrorf("specify only one of or ") + return cmdutil.FlagErrorf("specify only one of `` or `--job`") } if runF != nil { @@ -60,7 +60,7 @@ func NewCmdRerun(f *cmdutil.Factory, runF func(*RerunOptions) error) *cobra.Comm }, } - cmd.Flags().BoolVar(&opts.OnlyFailed, "failed", false, "Rerun only failed jobs") + 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 @@ -107,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 ``") } runID, err = shared.PromptForRun(cs, runs) if err != nil { From f6d2f83938b54a38441500c9fb34a25fd8a1850a Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Tue, 15 Mar 2022 01:44:51 -0400 Subject: [PATCH 65/83] Support setting Dependabot secrets (#5134) * Support setting Dependabot secrets * lint: Remove unnecessary assignment * Use `StringEnumFlag` helper for Application input * Add Dependabot to command description * Move repository name mapping after input validation * Error when multiple secret entities are set * Return an error for invalid apps * Use `assert` instead of `require` --- pkg/cmd/secret/secret.go | 5 +- pkg/cmd/secret/set/http.go | 19 +-- pkg/cmd/secret/set/set.go | 71 ++++++---- pkg/cmd/secret/set/set_test.go | 132 +++++++++++++---- pkg/cmd/secret/shared/shared.go | 83 +++++++++++ pkg/cmd/secret/shared/shared_test.go | 203 +++++++++++++++++++++++++++ 6 files changed, 446 insertions(+), 67 deletions(-) create mode 100644 pkg/cmd/secret/shared/shared_test.go diff --git a/pkg/cmd/secret/secret.go b/pkg/cmd/secret/secret.go index 2ef5eeec6..94231b661 100644 --- a/pkg/cmd/secret/secret.go +++ b/pkg/cmd/secret/secret.go @@ -14,8 +14,9 @@ func NewCmdSecret(f *cmdutil.Factory) *cobra.Command { Use: "secret ", 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. `), } diff --git a/pkg/cmd/secret/set/http.go b/pkg/cmd/secret/set/http.go index f74f757d9..f36c2b59e 100644 --- a/pkg/cmd/secret/set/http.go +++ b/pkg/cmd/secret/set/http.go @@ -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) } diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index 0c46d5f70..c147a81f5 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -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 `), @@ -150,6 +154,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command 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 } @@ -189,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 @@ -206,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 @@ -232,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) }() } @@ -257,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 } @@ -268,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 @@ -297,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) diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go index 7843a8b3b..72640a86a 100644 --- a/pkg/cmd/secret/set/set_test.go +++ b/pkg/cmd/secret/set/set_test.go @@ -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 { diff --git a/pkg/cmd/secret/shared/shared.go b/pkg/cmd/secret/shared/shared.go index 4f58dd971..4e5e70617 100644 --- a/pkg/cmd/secret/shared/shared.go +++ b/pkg/cmd/secret/shared/shared.go @@ -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 + } +} diff --git a/pkg/cmd/secret/shared/shared_test.go b/pkg/cmd/secret/shared/shared_test.go new file mode 100644 index 000000000..b247a85d9 --- /dev/null +++ b/pkg/cmd/secret/shared/shared_test.go @@ -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)) + } + }) + } +} From a20e8b7eec4005f3cf3e29f56ded121e90f675c6 Mon Sep 17 00:00:00 2001 From: Cameron Booth Date: Tue, 15 Mar 2022 09:50:20 -0700 Subject: [PATCH 66/83] Fix test I missed updating --- pkg/cmd/run/rerun/rerun_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/run/rerun/rerun_test.go b/pkg/cmd/run/rerun/rerun_test.go index b7bfc1d62..0f7c80997 100644 --- a/pkg/cmd/run/rerun/rerun_test.go +++ b/pkg/cmd/run/rerun/rerun_test.go @@ -233,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 ``", }, { name: "unrerunnable", From 3f65e5ae24c296423825a3d1ab557086ec84f4a2 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 11:46:36 -0400 Subject: [PATCH 67/83] Add pending opertion and reason to codespace response struct --- internal/codespaces/api/api.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index d8807ec74..e8cd0ac1f 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -168,17 +168,19 @@ func (a *API) GetRepository(ctx context.Context, nwo string) (*Repository, error // Codespace represents a codespace. 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"` - Machine CodespaceMachine `json:"machine"` - VSCSTarget string `json:"vscs_target"` + 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 { From 3d28c521044a39594683989f96bc4dd98f5fb910 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 11:47:10 -0400 Subject: [PATCH 68/83] Mark codespace with pending op as disabled with reason instead of state --- pkg/cmd/codespace/list.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 17c7b6414..43eeb74e4 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -89,10 +89,22 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er formattedName := formatNameForVSCSTarget(c.Name, c.VSCSTarget) - tp.AddField(formattedName, nil, cs.Yellow) + 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, cs.Gray) + } else { + tp.AddField(c.State, nil, stateColor) + } if tp.IsTTY() { ct, err := time.Parse(time.RFC3339, c.CreatedAt) From afa71c4b2fa7c99f0267a35046e8419c8a72db06 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 12:21:46 -0400 Subject: [PATCH 69/83] Disallow ssh'ing to codespace with a pending operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the API already disallows this, this makes the error cleaner and more explicit when a user is trying to start/ssh into a codespace that has a pending operation. Example of the old error message: ``` $ gh cs ssh -c cwndrws-redacted Starting codespace ⣽error connecting to codespace: error starting codespace: HTTP 422: your codespace has an operation pending: updating to a sku with a different amount of storage; please wait until this operation is complete (https://api.github.com/user/codespaces/cwndrws-redacted/start) exit status 1 ``` Example of the new error message: ``` $ gh cs ssh -c cwndrws-redacted codespace is disabled while it has a pending operation: Changing machine types... exit status 1 ``` --- pkg/cmd/codespace/ssh.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 6ce783f2f..af6c86614 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -128,6 +128,13 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e return fmt.Errorf("get or choose codespace: %w", err) } + if codespace.PendingOperation { + return fmt.Errorf( + "codespace is disabled while it has a pending operation: %s", + codespace.PendingOperationDisabledReason, + ) + } + liveshareLogger := noopLogger() if opts.debug { debugLogger, err := newFileLogger(opts.debugFile) From 599c7c900f69642e0927011227ac8d683179cf20 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 12:27:24 -0400 Subject: [PATCH 70/83] Disallow getting logs from codespaces with pending ops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the API already disallows this, this pretty much just cleans up the error reporting to the user. Example of old error: ``` $ gh cs logs -c cwndrws-redacted Starting codespace ⣽connecting to codespace: error starting codespace: HTTP 422: your codespace has an operation pending: updating to a sku with a different amount of storage; please wait until this operation is complete (https://api.github.com/user/codespaces/cwndrws-redacted/start) exit status 1 ``` Example of new error: ``` $ gh cs logs -c cwndrws-redacted codespace is disabled while it has a pending operation: Changing machine types... exit status 1 ``` --- pkg/cmd/codespace/logs.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/cmd/codespace/logs.go b/pkg/cmd/codespace/logs.go index d0a0c233b..221d95215 100644 --- a/pkg/cmd/codespace/logs.go +++ b/pkg/cmd/codespace/logs.go @@ -41,6 +41,13 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err return fmt.Errorf("get or choose codespace: %w", err) } + if codespace.PendingOperation { + return fmt.Errorf( + "codespace is disabled while it has a pending operation: %s", + codespace.PendingOperationDisabledReason, + ) + } + authkeys := make(chan error, 1) go func() { authkeys <- checkAuthorizedKeys(ctx, a.apiClient) From da99a1b59bc6f13aa7246ccf8814de8528bb172b Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 14:11:33 -0400 Subject: [PATCH 71/83] Ensure codespace exists and doesn't have a pending op when opening Code The initial intention for this change was to disallow users to open a codespace in VS Code if the codespace has a pending operation. This also adds a side-benefit of presenting the user an error before waiting for VS Code to open if they provide an invalid codespace to open. --- pkg/cmd/codespace/code.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/codespace/code.go b/pkg/cmd/codespace/code.go index f80e32527..7b52db29f 100644 --- a/pkg/cmd/codespace/code.go +++ b/pkg/cmd/codespace/code.go @@ -31,18 +31,19 @@ func newCodeCmd(app *App) *cobra.Command { // VSCode opens a codespace in the local VS VSCode application. func (a *App) VSCode(ctx context.Context, 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 + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + if err != nil { + return fmt.Errorf("get or choose codespace: %w", err) } - url := vscodeProtocolURL(codespaceName, useInsiders) + if codespace.PendingOperation { + return fmt.Errorf( + "codespace is disabled while it has a pending operation: %s", + codespace.PendingOperationDisabledReason, + ) + } + + url := vscodeProtocolURL(codespace.Name, useInsiders) if err := a.browser.Browse(url); err != nil { return fmt.Errorf("error opening Visual Studio Code: %w", err) } From 5ffe838dce24b5456e804e76a349bdcc70f68eea Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 15:24:35 -0400 Subject: [PATCH 72/83] Disallow any port operations when codespace has pending operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since all of the port operations require the codespace to be running, we need to disallow these operations when there's a pending op since we can't start the codespace in this state. Since the API already disallows this, this is basically cleaning up the error messages that the user sees in this state Old error message: ``` $ gh cs ports forward 80:80 ? Choose codespace: redacted Starting codespace ⣻error connecting to codespace: error starting codespace: HTTP 422: your codespace has an operation pending: updating to a sku with a different amount of storage; please wait until this operation is complete (https://api.github.com/user/codespaces/cwndrws-redacted/start) ``` New error message: ``` $ gh cs ports forward 80:80 ? Choose codespace: redacted codespace is disabled while it has a pending operation: Changing machine types... exit status 1 ``` --- pkg/cmd/codespace/ports.go | 42 +++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index 094833e30..cc4365742 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -46,13 +46,9 @@ func newPortsCmd(app *App) *cobra.Command { // ListPorts lists known ports in a codespace. func (a *App) ListPorts(ctx context.Context, codespaceName string, exporter cmdutil.Exporter) (err error) { - codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + codespace, err := getCodespaceForPorts(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) @@ -235,12 +231,9 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar return fmt.Errorf("error parsing port arguments: %w", err) } - codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + codespace, err := getCodespaceForPorts(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) @@ -311,12 +304,9 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st return fmt.Errorf("get port pairs: %w", err) } - codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) + codespace, err := getCodespaceForPorts(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) @@ -380,3 +370,23 @@ func normalizeJSON(j []byte) []byte { // remove trailing commas return bytes.ReplaceAll(j, []byte("},}"), []byte("}}")) } + +func getCodespaceForPorts(ctx context.Context, apiClient apiClient, codespaceName string) (*api.Codespace, error) { + codespace, err := getOrChooseCodespace(ctx, apiClient, codespaceName) + if err != nil { + // TODO(josebalius): remove special handling of this error here and it other places + if err == errNoCodespaces { + return nil, err + } + return nil, fmt.Errorf("error choosing codespace: %w", err) + } + + if codespace.PendingOperation { + return nil, fmt.Errorf( + "codespace is disabled while it has a pending operation: %s", + codespace.PendingOperationDisabledReason, + ) + } + + return codespace, nil +} From 27a5512b41aa4d000b1e62377332c40939d931a5 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 16:58:50 -0400 Subject: [PATCH 73/83] Add test for disallowing ssh when codespace has a pending op --- pkg/cmd/codespace/ssh_test.go | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 pkg/cmd/codespace/ssh_test.go diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go new file mode 100644 index 000000000..3b242ae4b --- /dev/null +++ b/pkg/cmd/codespace/ssh_test.go @@ -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) +} From 6346779f3521090e053bf58c7decfb47c9ff243e Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 17:03:54 -0400 Subject: [PATCH 74/83] Add test for disallowing logs when codespace has a pending op --- pkg/cmd/codespace/logs_test.go | 47 ++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 pkg/cmd/codespace/logs_test.go diff --git a/pkg/cmd/codespace/logs_test.go b/pkg/cmd/codespace/logs_test.go new file mode 100644 index 000000000..1eaf2e9d9 --- /dev/null +++ b/pkg/cmd/codespace/logs_test.go @@ -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) +} From f94a1a2bd49b7e6c7b747032b43214f45a4c7d45 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 17:12:16 -0400 Subject: [PATCH 75/83] Add test for disallowing opening vs code for codespace with pending op I also refactored the existing vs code test a bit to work with the new use of `getOrChooseCodespace`. --- pkg/cmd/codespace/code_test.go | 48 +++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/code_test.go b/pkg/cmd/codespace/code_test.go index cbc59864a..e35060c73 100644 --- a/pkg/cmd/codespace/code_test.go +++ b/pkg/cmd/codespace/code_test.go @@ -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) { @@ -41,7 +43,8 @@ func TestApp_VSCode(t *testing.T) { t.Run(tt.name, func(t *testing.T) { b := &cmdutil.TestBrowser{} a := &App{ - browser: b, + 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) @@ -50,3 +53,46 @@ func TestApp_VSCode(t *testing.T) { }) } } + +func TestPendingOperationDisallowsCode(t *testing.T) { + app := testingLogsApp() + + 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 + }, + } +} From 3ed2e49bd95f324abbd01d09a72b7ab7d8274d97 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Tue, 15 Mar 2022 17:17:57 -0400 Subject: [PATCH 76/83] Add tests for disallowing all port commands for codespace w/ pending op --- pkg/cmd/codespace/ports_test.go | 71 +++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 pkg/cmd/codespace/ports_test.go diff --git a/pkg/cmd/codespace/ports_test.go b/pkg/cmd/codespace/ports_test.go new file mode 100644 index 000000000..f1487ed2e --- /dev/null +++ b/pkg/cmd/codespace/ports_test.go @@ -0,0 +1,71 @@ +package codespace + +import ( + "context" + "testing" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/cli/cli/v2/pkg/iostreams" +) + +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) +} From 10e43b51364de4cad6d6d79bed3dd9d493bc86f2 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 08:43:34 -0400 Subject: [PATCH 77/83] Use color variable instead of literal for disabled reason --- pkg/cmd/codespace/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go index 43eeb74e4..41f44d299 100644 --- a/pkg/cmd/codespace/list.go +++ b/pkg/cmd/codespace/list.go @@ -101,7 +101,7 @@ func (a *App) List(ctx context.Context, limit int, exporter cmdutil.Exporter) er tp.AddField(c.Repository.FullName, nil, nil) tp.AddField(c.branchWithGitStatus(), nil, cs.Cyan) if c.PendingOperation { - tp.AddField(c.PendingOperationDisabledReason, nil, cs.Gray) + tp.AddField(c.PendingOperationDisabledReason, nil, nameColor) } else { tp.AddField(c.State, nil, stateColor) } From 8bf0cb8f13bafff98b6be64db208542ac4d65eb2 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 08:51:33 -0400 Subject: [PATCH 78/83] Refactor the getOrChooseCodespace to always check for pending ops --- pkg/cmd/codespace/code.go | 7 ------- pkg/cmd/codespace/code_test.go | 2 +- pkg/cmd/codespace/common.go | 7 +++++++ pkg/cmd/codespace/logs.go | 7 ------- pkg/cmd/codespace/logs_test.go | 2 +- pkg/cmd/codespace/ports.go | 26 +++----------------------- pkg/cmd/codespace/ssh.go | 7 ------- pkg/cmd/codespace/ssh_test.go | 2 +- 8 files changed, 13 insertions(+), 47 deletions(-) diff --git a/pkg/cmd/codespace/code.go b/pkg/cmd/codespace/code.go index 7b52db29f..ba2455385 100644 --- a/pkg/cmd/codespace/code.go +++ b/pkg/cmd/codespace/code.go @@ -36,13 +36,6 @@ func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool return fmt.Errorf("get or choose codespace: %w", err) } - if codespace.PendingOperation { - return fmt.Errorf( - "codespace is disabled while it has a pending operation: %s", - codespace.PendingOperationDisabledReason, - ) - } - url := vscodeProtocolURL(codespace.Name, useInsiders) if err := a.browser.Browse(url); err != nil { return fmt.Errorf("error opening Visual Studio Code: %w", err) diff --git a/pkg/cmd/codespace/code_test.go b/pkg/cmd/codespace/code_test.go index e35060c73..0eb3b7ade 100644 --- a/pkg/cmd/codespace/code_test.go +++ b/pkg/cmd/codespace/code_test.go @@ -58,7 +58,7 @@ func TestPendingOperationDisallowsCode(t *testing.T) { app := testingLogsApp() 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" { + if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { t.Errorf("expected pending operation error, but got: %v", err) } } else { diff --git a/pkg/cmd/codespace/common.go b/pkg/cmd/codespace/common.go index 6061eac78..aec386f6c 100644 --- a/pkg/cmd/codespace/common.go +++ b/pkg/cmd/codespace/common.go @@ -183,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 } diff --git a/pkg/cmd/codespace/logs.go b/pkg/cmd/codespace/logs.go index 221d95215..d0a0c233b 100644 --- a/pkg/cmd/codespace/logs.go +++ b/pkg/cmd/codespace/logs.go @@ -41,13 +41,6 @@ func (a *App) Logs(ctx context.Context, codespaceName string, follow bool) (err return fmt.Errorf("get or choose codespace: %w", err) } - if codespace.PendingOperation { - return fmt.Errorf( - "codespace is disabled while it has a pending operation: %s", - codespace.PendingOperationDisabledReason, - ) - } - authkeys := make(chan error, 1) go func() { authkeys <- checkAuthorizedKeys(ctx, a.apiClient) diff --git a/pkg/cmd/codespace/logs_test.go b/pkg/cmd/codespace/logs_test.go index 1eaf2e9d9..225e10680 100644 --- a/pkg/cmd/codespace/logs_test.go +++ b/pkg/cmd/codespace/logs_test.go @@ -12,7 +12,7 @@ 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" { + if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { t.Errorf("expected pending operation error, but got: %v", err) } } else { diff --git a/pkg/cmd/codespace/ports.go b/pkg/cmd/codespace/ports.go index cc4365742..274be8d7b 100644 --- a/pkg/cmd/codespace/ports.go +++ b/pkg/cmd/codespace/ports.go @@ -46,7 +46,7 @@ func newPortsCmd(app *App) *cobra.Command { // ListPorts lists known ports in a codespace. func (a *App) ListPorts(ctx context.Context, codespaceName string, exporter cmdutil.Exporter) (err error) { - codespace, err := getCodespaceForPorts(ctx, a.apiClient, codespaceName) + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { return err } @@ -231,7 +231,7 @@ func (a *App) UpdatePortVisibility(ctx context.Context, codespaceName string, ar return fmt.Errorf("error parsing port arguments: %w", err) } - codespace, err := getCodespaceForPorts(ctx, a.apiClient, codespaceName) + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { return err } @@ -304,7 +304,7 @@ func (a *App) ForwardPorts(ctx context.Context, codespaceName string, ports []st return fmt.Errorf("get port pairs: %w", err) } - codespace, err := getCodespaceForPorts(ctx, a.apiClient, codespaceName) + codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { return err } @@ -370,23 +370,3 @@ func normalizeJSON(j []byte) []byte { // remove trailing commas return bytes.ReplaceAll(j, []byte("},}"), []byte("}}")) } - -func getCodespaceForPorts(ctx context.Context, apiClient apiClient, codespaceName string) (*api.Codespace, error) { - codespace, err := getOrChooseCodespace(ctx, apiClient, codespaceName) - if err != nil { - // TODO(josebalius): remove special handling of this error here and it other places - if err == errNoCodespaces { - return nil, err - } - return nil, fmt.Errorf("error choosing codespace: %w", err) - } - - if codespace.PendingOperation { - return nil, fmt.Errorf( - "codespace is disabled while it has a pending operation: %s", - codespace.PendingOperationDisabledReason, - ) - } - - return codespace, nil -} diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index af6c86614..6ce783f2f 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -128,13 +128,6 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e return fmt.Errorf("get or choose codespace: %w", err) } - if codespace.PendingOperation { - return fmt.Errorf( - "codespace is disabled while it has a pending operation: %s", - codespace.PendingOperationDisabledReason, - ) - } - liveshareLogger := noopLogger() if opts.debug { debugLogger, err := newFileLogger(opts.debugFile) diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index 3b242ae4b..fac5147bb 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -12,7 +12,7 @@ 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" { + if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { t.Errorf("expected pending operation error, but got: %v", err) } } else { From a2f76fdfe0e92e403ff301d668a96939c997b80b Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 09:10:54 -0400 Subject: [PATCH 79/83] Fix copy pasta error to appease the linter --- pkg/cmd/codespace/code_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/codespace/code_test.go b/pkg/cmd/codespace/code_test.go index 0eb3b7ade..eda616a20 100644 --- a/pkg/cmd/codespace/code_test.go +++ b/pkg/cmd/codespace/code_test.go @@ -55,7 +55,7 @@ func TestApp_VSCode(t *testing.T) { } func TestPendingOperationDisallowsCode(t *testing.T) { - app := testingLogsApp() + app := testingCodeApp() if err := app.VSCode(context.Background(), "disabledCodespace", false); err != nil { if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { From 64eecef17636627abbc20ff07ac09fcd1244637b Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 09:36:14 -0400 Subject: [PATCH 80/83] Remove unhelpful error wrapper --- pkg/cmd/codespace/code.go | 2 +- pkg/cmd/codespace/code_test.go | 2 +- pkg/cmd/codespace/logs.go | 2 +- pkg/cmd/codespace/logs_test.go | 2 +- pkg/cmd/codespace/ssh.go | 2 +- pkg/cmd/codespace/ssh_test.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/codespace/code.go b/pkg/cmd/codespace/code.go index ba2455385..0942d15d3 100644 --- a/pkg/cmd/codespace/code.go +++ b/pkg/cmd/codespace/code.go @@ -33,7 +33,7 @@ func newCodeCmd(app *App) *cobra.Command { func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool) error { codespace, err := getOrChooseCodespace(ctx, a.apiClient, codespaceName) if err != nil { - return fmt.Errorf("get or choose codespace: %w", err) + return err } url := vscodeProtocolURL(codespace.Name, useInsiders) diff --git a/pkg/cmd/codespace/code_test.go b/pkg/cmd/codespace/code_test.go index eda616a20..30f06bd39 100644 --- a/pkg/cmd/codespace/code_test.go +++ b/pkg/cmd/codespace/code_test.go @@ -58,7 +58,7 @@ func TestPendingOperationDisallowsCode(t *testing.T) { app := testingCodeApp() if err := app.VSCode(context.Background(), "disabledCodespace", false); err != nil { - if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { + 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 { diff --git a/pkg/cmd/codespace/logs.go b/pkg/cmd/codespace/logs.go index d0a0c233b..6feab3080 100644 --- a/pkg/cmd/codespace/logs.go +++ b/pkg/cmd/codespace/logs.go @@ -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) diff --git a/pkg/cmd/codespace/logs_test.go b/pkg/cmd/codespace/logs_test.go index 225e10680..1eaf2e9d9 100644 --- a/pkg/cmd/codespace/logs_test.go +++ b/pkg/cmd/codespace/logs_test.go @@ -12,7 +12,7 @@ func TestPendingOperationDisallowsLogs(t *testing.T) { app := testingLogsApp() if err := app.Logs(context.Background(), "disabledCodespace", false); err != nil { - if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { + 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 { diff --git a/pkg/cmd/codespace/ssh.go b/pkg/cmd/codespace/ssh.go index 6ce783f2f..dfbc75694 100644 --- a/pkg/cmd/codespace/ssh.go +++ b/pkg/cmd/codespace/ssh.go @@ -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() diff --git a/pkg/cmd/codespace/ssh_test.go b/pkg/cmd/codespace/ssh_test.go index fac5147bb..3b242ae4b 100644 --- a/pkg/cmd/codespace/ssh_test.go +++ b/pkg/cmd/codespace/ssh_test.go @@ -12,7 +12,7 @@ func TestPendingOperationDisallowsSSH(t *testing.T) { app := testingSSHApp() if err := app.SSH(context.Background(), []string{}, sshOptions{codespace: "disabledCodespace"}); err != nil { - if err.Error() != "get or choose codespace: codespace is disabled while it has a pending operation: Some pending operation" { + 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 { From 7ca31e02b4feb25c903b7811fec0745815742320 Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 10:13:46 -0400 Subject: [PATCH 81/83] Disallow editing a codespace that has a pending operation This is already prevented by the API, but this will make the error message cleaner and easier to understand for the user. Example of old error message: ``` $ gh cs edit -c cwndrws-redacted -d "some silly name" error editing codespace: HTTP 422: your codespace has an operation pending: updating to a sku with a different amount of storage; please wait until this operation is complete (https://api.github.com/user/codespaces/cwndrws-redacted) exit status 1 ``` Example of new error message: ``` $ gh cs edit -c cwndrws-redacted -d "some silly name" error editing codespace: codespace is disabled while it has a pending operation: Changing machine types... exit status 1 ``` --- internal/codespaces/api/api.go | 22 ++++++++++++++++ internal/codespaces/api/api_test.go | 39 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index e8cd0ac1f..e9ef4c339 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -783,6 +783,20 @@ func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *E 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) } @@ -799,6 +813,14 @@ func (a *API) EditCodespace(ctx context.Context, codespaceName string, params *E 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"` } diff --git a/internal/codespaces/api/api_test.go b/internal/codespaces/api/api_test.go index ebbbe5209..81eb563c3 100644 --- a/internal/codespaces/api/api_test.go +++ b/internal/codespaces/api/api_test.go @@ -388,6 +388,7 @@ func createFakeEditServer(t *testing.T, codespaceName string) *httptest.Server { fmt.Fprint(w, string(responseData)) })) } + func TestAPI_EditCodespace(t *testing.T) { type args struct { ctx context.Context @@ -434,3 +435,41 @@ func TestAPI_EditCodespace(t *testing.T) { }) } } + +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) + } +} From 4eedfc05c1f672aeb0f75f29eb5bb0c762abb8be Mon Sep 17 00:00:00 2001 From: Charlie Andrews Date: Wed, 16 Mar 2022 10:20:13 -0400 Subject: [PATCH 82/83] Add docs link comment to Codespaces struct definition --- internal/codespaces/api/api.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index e9ef4c339..e8c76e6be 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -167,6 +167,8 @@ 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"` From 311b9bd38042be530bffe4c119eb8e6dd8e1f531 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Mar 2022 14:25:15 +0000 Subject: [PATCH 83/83] Bump github.com/stretchr/testify from 1.7.0 to 1.7.1 Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.0 to 1.7.1. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.7.0...v1.7.1) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index cd63fd72b..e34008e52 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/sourcegraph/jsonrpc2 v0.1.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 diff --git a/go.sum b/go.sum index f51a48221..5ad634436 100644 --- a/go.sum +++ b/go.sum @@ -224,8 +224,9 @@ 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/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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=