Merge branch 'trunk' into attestation-bundle-fetch-improvements
This commit is contained in:
commit
840fe2198c
63 changed files with 2498 additions and 615 deletions
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -9,7 +9,11 @@ assignees: ''
|
|||
|
||||
### Describe the bug
|
||||
|
||||
A clear and concise description of what the bug is. Include version by typing `gh --version`.
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### Affected version
|
||||
|
||||
Please run `gh version` and paste the output below.
|
||||
|
||||
### Steps to reproduce the behavior
|
||||
|
||||
|
|
|
|||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
go mod verify
|
||||
go mod download
|
||||
|
||||
LINT_VERSION=1.59.1
|
||||
LINT_VERSION=1.63.4
|
||||
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/
|
||||
|
|
@ -53,6 +53,6 @@ jobs:
|
|||
assert-nothing-changed go fmt ./...
|
||||
assert-nothing-changed go mod tidy
|
||||
|
||||
bin/golangci-lint run --out-format=github-actions --timeout=3m || STATUS=$?
|
||||
bin/golangci-lint run --out-format=colored-line-number --timeout=3m || STATUS=$?
|
||||
|
||||
exit $STATUS
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ Download packaged binaries from the [releases page][].
|
|||
|
||||
Since version 2.50.0 `gh` has been producing [Build Provenance Attestation](https://github.blog/changelog/2024-06-25-artifact-attestations-is-generally-available/) enabling a cryptographically verifiable paper-trail back to the origin GitHub repository, git revision and build instructions used. The build provenance attestations are signed and relies on Public Good [Sigstore](https://www.sigstore.dev/) for PKI.
|
||||
|
||||
There are two common ways to verify a downloaded release, depending if `gh` is aready installed or not. If `gh` is installed, it's trivial to verify a new release:
|
||||
There are two common ways to verify a downloaded release, depending if `gh` is already installed or not. If `gh` is installed, it's trivial to verify a new release:
|
||||
|
||||
- **Option 1: Using `gh` if already installed:**
|
||||
|
||||
|
|
|
|||
36
acceptance/testdata/secret/secret-require-remote-disambiguation.txtar
vendored
Normal file
36
acceptance/testdata/secret/secret-require-remote-disambiguation.txtar
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Set up env vars
|
||||
env REPO=${SCRIPT_NAME}-${RANDOM_STRING}
|
||||
|
||||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create ${ORG}/${REPO} --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}
|
||||
|
||||
# Create a fork
|
||||
exec gh repo fork ${ORG}/${REPO} --org ${ORG} --fork-name ${REPO}-fork
|
||||
|
||||
# Defer fork cleanup
|
||||
defer gh repo delete --yes ${ORG}/${REPO}-fork
|
||||
|
||||
# Sleep to allow the fork to be created before cloning
|
||||
sleep 2
|
||||
|
||||
# Clone and move into the fork repo
|
||||
exec gh repo clone ${ORG}/${REPO}-fork
|
||||
cd ${REPO}-fork
|
||||
|
||||
# Secret list requires disambiguation
|
||||
! exec gh secret list
|
||||
stderr 'multiple remotes detected. please specify which repo to use by providing the -R, --repo argument'
|
||||
|
||||
# Secret set requires disambiguation
|
||||
! exec gh secret set 'TEST_SECRET_NAME' --body 'TEST_SECRET_VALUE'
|
||||
stderr 'multiple remotes detected. please specify which repo to use by providing the -R, --repo argument'
|
||||
|
||||
# Secret delete requires disambiguation
|
||||
! exec gh secret delete 'TEST_SECRET_NAME'
|
||||
stderr 'multiple remotes detected. please specify which repo to use by providing the -R, --repo argument'
|
||||
|
|
@ -275,6 +275,13 @@ Void Linux users can install from the [official distribution repo](https://voidl
|
|||
sudo xbps-install github-cli
|
||||
```
|
||||
|
||||
### Manjaro Linux
|
||||
Manjaro Linux users can install from the [official extra repository](https://manjaristas.org/branch_compare?q=github-cli):
|
||||
|
||||
```bash
|
||||
pamac install github-cli
|
||||
```
|
||||
|
||||
[releases page]: https://github.com/cli/cli/releases/latest
|
||||
[arch linux repo]: https://www.archlinux.org/packages/extra/x86_64/github-cli
|
||||
[arch linux aur]: https://aur.archlinux.org/packages/github-cli-git
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Installation from source
|
||||
|
||||
1. Verify that you have Go 1.22+ installed
|
||||
1. Verify that you have Go 1.23+ installed
|
||||
|
||||
```sh
|
||||
$ go version
|
||||
|
|
|
|||
|
|
@ -377,19 +377,36 @@ func (c *Client) lookupCommit(ctx context.Context, sha, format string) ([]byte,
|
|||
}
|
||||
|
||||
// ReadBranchConfig parses the `branch.BRANCH.(remote|merge|gh-merge-base)` part of git config.
|
||||
func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg BranchConfig) {
|
||||
// If no branch config is found or there is an error in the command, it returns an empty BranchConfig.
|
||||
// Downstream consumers of ReadBranchConfig should consider the behavior they desire if this errors,
|
||||
// as an empty config is not necessarily breaking.
|
||||
func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (BranchConfig, error) {
|
||||
|
||||
prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch))
|
||||
args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge|%s)$", prefix, MergeBaseConfig)}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return
|
||||
return BranchConfig{}, err
|
||||
}
|
||||
|
||||
for _, line := range outputLines(out) {
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
// This is the error we expect if the git command does not run successfully.
|
||||
// If the ExitCode is 1, then we just didn't find any config for the branch.
|
||||
var gitError *GitError
|
||||
if ok := errors.As(err, &gitError); ok && gitError.ExitCode != 1 {
|
||||
return BranchConfig{}, err
|
||||
}
|
||||
return BranchConfig{}, nil
|
||||
}
|
||||
|
||||
return parseBranchConfig(outputLines(out)), nil
|
||||
}
|
||||
|
||||
func parseBranchConfig(configLines []string) BranchConfig {
|
||||
var cfg BranchConfig
|
||||
|
||||
for _, line := range configLines {
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
|
|
@ -412,7 +429,7 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg Branc
|
|||
cfg.MergeBase = parts[1]
|
||||
}
|
||||
}
|
||||
return
|
||||
return cfg
|
||||
}
|
||||
|
||||
// SetBranchConfig sets the named value on the given branch.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -730,14 +731,35 @@ func TestClientReadBranchConfig(t *testing.T) {
|
|||
cmdExitStatus int
|
||||
cmdStdout string
|
||||
cmdStderr string
|
||||
wantCmdArgs string
|
||||
branch string
|
||||
wantBranchConfig BranchConfig
|
||||
wantError *GitError
|
||||
}{
|
||||
{
|
||||
name: "read branch config",
|
||||
cmdExitStatus: 0,
|
||||
cmdStdout: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base trunk",
|
||||
wantCmdArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|gh-merge-base)$`,
|
||||
branch: "trunk",
|
||||
wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk"},
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "git config runs successfully but returns no output (Exit Code 1)",
|
||||
cmdExitStatus: 1,
|
||||
cmdStdout: "",
|
||||
cmdStderr: "",
|
||||
branch: "trunk",
|
||||
wantBranchConfig: BranchConfig{},
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "output error (Exit Code > 1)",
|
||||
cmdExitStatus: 2,
|
||||
cmdStdout: "",
|
||||
cmdStderr: "git error message",
|
||||
branch: "trunk",
|
||||
wantBranchConfig: BranchConfig{},
|
||||
wantError: &GitError{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
@ -747,9 +769,85 @@ func TestClientReadBranchConfig(t *testing.T) {
|
|||
GitPath: "path/to/git",
|
||||
commandContext: cmdCtx,
|
||||
}
|
||||
branchConfig := client.ReadBranchConfig(context.Background(), "trunk")
|
||||
assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
|
||||
branchConfig, err := client.ReadBranchConfig(context.Background(), tt.branch)
|
||||
wantCmdArgs := fmt.Sprintf("path/to/git config --get-regexp ^branch\\.%s\\.(remote|merge|gh-merge-base)$", tt.branch)
|
||||
assert.Equal(t, wantCmdArgs, strings.Join(cmd.Args[3:], " "))
|
||||
assert.Equal(t, tt.wantBranchConfig, branchConfig)
|
||||
if tt.wantError != nil {
|
||||
assert.ErrorAs(t, err, &tt.wantError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseBranchConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configLines []string
|
||||
wantBranchConfig BranchConfig
|
||||
}{
|
||||
{
|
||||
name: "remote branch",
|
||||
configLines: []string{"branch.trunk.remote origin"},
|
||||
wantBranchConfig: BranchConfig{
|
||||
RemoteName: "origin",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge ref",
|
||||
configLines: []string{"branch.trunk.merge refs/heads/trunk"},
|
||||
wantBranchConfig: BranchConfig{
|
||||
MergeRef: "refs/heads/trunk",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge base",
|
||||
configLines: []string{"branch.trunk.gh-merge-base gh-merge-base"},
|
||||
wantBranchConfig: BranchConfig{
|
||||
MergeBase: "gh-merge-base",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote, merge ref, and merge base all specified",
|
||||
configLines: []string{
|
||||
"branch.trunk.remote origin",
|
||||
"branch.trunk.merge refs/heads/trunk",
|
||||
"branch.trunk.gh-merge-base gh-merge-base",
|
||||
},
|
||||
wantBranchConfig: BranchConfig{
|
||||
RemoteName: "origin",
|
||||
MergeRef: "refs/heads/trunk",
|
||||
MergeBase: "gh-merge-base",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote URL",
|
||||
configLines: []string{
|
||||
"branch.Frederick888/main.remote git@github.com:Frederick888/playground.git",
|
||||
"branch.Frederick888/main.merge refs/heads/main",
|
||||
},
|
||||
wantBranchConfig: BranchConfig{
|
||||
MergeRef: "refs/heads/main",
|
||||
RemoteURL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "/Frederick888/playground.git",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
branchConfig := parseBranchConfig(tt.configLines)
|
||||
assert.Equal(t, tt.wantBranchConfig.RemoteName, branchConfig.RemoteName)
|
||||
assert.Equal(t, tt.wantBranchConfig.MergeRef, branchConfig.MergeRef)
|
||||
assert.Equal(t, tt.wantBranchConfig.MergeBase, branchConfig.MergeBase)
|
||||
if tt.wantBranchConfig.RemoteURL != nil {
|
||||
assert.Equal(t, tt.wantBranchConfig.RemoteURL.String(), branchConfig.RemoteURL.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
70
go.mod
70
go.mod
|
|
@ -1,16 +1,16 @@
|
|||
module github.com/cli/cli/v2
|
||||
|
||||
go 1.22.5
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.22.6
|
||||
toolchain go1.23.5
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/MakeNowJust/heredoc v1.0.0
|
||||
github.com/briandowns/spinner v1.18.1
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/charmbracelet/glamour v0.7.0
|
||||
github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c
|
||||
github.com/charmbracelet/glamour v0.8.0
|
||||
github.com/charmbracelet/lipgloss v0.12.1
|
||||
github.com/cli/go-gh/v2 v2.11.2
|
||||
github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24
|
||||
github.com/cli/oauth v1.1.1
|
||||
|
|
@ -18,12 +18,12 @@ require (
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.6
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7
|
||||
github.com/distribution/reference v0.5.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.7
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.8
|
||||
github.com/gdamore/tcell/v2 v2.5.4
|
||||
github.com/golang/snappy v0.0.4
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/go-containerregistry v0.20.2
|
||||
github.com/google/go-containerregistry v0.20.3
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
|
|
@ -40,47 +40,47 @@ require (
|
|||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
|
||||
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
|
||||
github.com/sigstore/protobuf-specs v0.3.2
|
||||
github.com/sigstore/protobuf-specs v0.3.3
|
||||
github.com/sigstore/sigstore-go v0.6.2
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/zalando/go-keyring v0.2.5
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/term v0.27.0
|
||||
golang.org/x/term v0.28.0
|
||||
golang.org/x/text v0.21.0
|
||||
google.golang.org/grpc v1.64.1
|
||||
google.golang.org/protobuf v1.34.2
|
||||
google.golang.org/protobuf v1.36.3
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||
github.com/alessio/shellescape v1.4.2 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f // indirect
|
||||
github.com/charmbracelet/x/ansi v0.1.4 // indirect
|
||||
github.com/cli/browser v1.3.0 // indirect
|
||||
github.com/cli/shurcooL-graphql v0.0.4 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 // indirect
|
||||
github.com/danieljoos/wincred v1.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/docker/cli v27.1.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/docker/cli v27.5.0+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
github.com/go-openapi/errors v0.22.0 // indirect
|
||||
|
|
@ -95,7 +95,7 @@ require (
|
|||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.2.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
|
|
@ -107,21 +107,20 @@ require (
|
|||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
|
|
@ -149,20 +148,21 @@ require (
|
|||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
|
||||
github.com/transparency-dev/merkle v0.0.2 // indirect
|
||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||
github.com/yuin/goldmark v1.5.4 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.2 // indirect
|
||||
github.com/vbatts/tar-split v0.11.6 // indirect
|
||||
github.com/yuin/goldmark v1.7.4 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.3 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.27.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
|
|
|||
174
go.sum
174
go.sum
|
|
@ -1,8 +1,7 @@
|
|||
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
|
||||
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
|
||||
cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
|
||||
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
|
||||
cloud.google.com/go/kms v1.15.8 h1:szIeDCowID8th2i8XE4uRev5PMxQFqW+JjwYxL9h6xs=
|
||||
|
|
@ -25,17 +24,16 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occ
|
|||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
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-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
|
||||
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
|
|
@ -72,6 +70,8 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
|||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
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 v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
|
|
@ -86,12 +86,14 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
|
|||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
|
||||
github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
|
||||
github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c h1:0FwZb0wTiyalb8QQlILWyIuh3nF5wok6j9D9oUQwfQY=
|
||||
github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c/go.mod h1:EPP2QJ0ectp3zo6gx9f8oJGq8keirqPJ3XpYEI8wrrs=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f h1:1BXkZqDueTOBECyDoFGRi0xMYgjJ6vvoPIkWyKOwzTc=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0=
|
||||
github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
|
||||
github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
|
||||
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
|
||||
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
|
||||
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
|
||||
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4 h1:6KzMkQeAF56rggw2NZu1L+TH7j9+DM1/2Kmh7KUxg1I=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
|
||||
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||
|
|
@ -108,9 +110,8 @@ github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJ
|
|||
github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
|
||||
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
|
|
@ -130,16 +131,16 @@ github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQM
|
|||
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc=
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I=
|
||||
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y=
|
||||
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE=
|
||||
github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
|
||||
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
|
||||
github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
|
|
@ -149,8 +150,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
|||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
|
||||
|
|
@ -162,8 +163,8 @@ github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQr
|
|||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
|
||||
|
|
@ -204,8 +205,8 @@ github.com/google/certificate-transparency-go v1.2.1 h1:4iW/NwzqOqYEEoCBEFP+jPbB
|
|||
github.com/google/certificate-transparency-go v1.2.1/go.mod h1:bvn/ytAccv+I6+DGkqpvSsEdiVGramgaSC6RD3tEmeE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
|
||||
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
|
||||
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
|
||||
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
|
|
@ -222,8 +223,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF
|
|||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
|
||||
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/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
|
|
@ -288,8 +289,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
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/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
|
|
@ -318,8 +319,8 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
|||
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.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/microsoft/dev-tunnels v0.0.25 h1:UlMKUI+2O8cSu4RlB52ioSyn1LthYSVkJA+CSTsdKoA=
|
||||
github.com/microsoft/dev-tunnels v0.0.25/go.mod h1:frU++12T/oqxckXkDpTuYa427ncguEOodSPZcGCCrzQ=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
|
|
@ -328,20 +329,18 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
|
|||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
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.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
|
||||
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/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
|
|
@ -369,8 +368,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ=
|
||||
github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
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/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
|
|
@ -393,8 +392,8 @@ github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc h1:vH0NQbIDk+mJL
|
|||
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
|
||||
github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWwQvORuRQo=
|
||||
github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA=
|
||||
github.com/sigstore/protobuf-specs v0.3.3 h1:RMZQgXTD/pF7KW6b5NaRLYxFYZ/wzx44PQFXN2PEo5g=
|
||||
github.com/sigstore/protobuf-specs v0.3.3/go.mod h1:vIhZ6Uor1a38+wvRrKcqL2PtYNlgoIW9lhzYzkyy4EU=
|
||||
github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
|
||||
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
|
||||
github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk=
|
||||
|
|
@ -411,7 +410,6 @@ github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.3 h1:h9G8j+Ds21zq
|
|||
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.3/go.mod h1:zgCeHOuqF6k7A7TTEvftcA9V3FRzB7mrPtHOhXAQBnc=
|
||||
github.com/sigstore/timestamp-authority v1.2.2 h1:X4qyutnCQqJ0apMewFyx+3t7Tws00JQ/JonBiu3QvLE=
|
||||
github.com/sigstore/timestamp-authority v1.2.2/go.mod h1:nEah4Eq4wpliDjlY342rXclGSO7Kb9hoRrl9tqLW13A=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
|
|
@ -436,10 +434,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
|
||||
|
|
@ -452,33 +449,34 @@ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C
|
|||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
|
||||
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
|
||||
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
|
||||
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
|
||||
github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
|
||||
github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
|
||||
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
|
||||
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
||||
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
||||
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
|
||||
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4=
|
||||
github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=
|
||||
github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
|
||||
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
||||
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 h1:vS1Ao/R55RNV4O7TA2Qopok8yN+X0LIP6RVWLFkprck=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0/go.mod h1:BMsdeOxN04K0L5FNUBfjFdvwWGNe/rkmSwH4Aelu/X0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
|
||||
go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
|
||||
go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
|
||||
go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
|
||||
go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
|
||||
go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI=
|
||||
go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A=
|
||||
go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
|
||||
go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.step.sm/crypto v0.44.2 h1:t3p3uQ7raP2jp2ha9P6xkQF85TJZh+87xmjSLaib+jk=
|
||||
go.step.sm/crypto v0.44.2/go.mod h1:x1439EnFhadzhkuaGX7sz03LEMQ+jV4gRamf5LCZJQQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
|
|
@ -489,20 +487,20 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
|||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
|
|
@ -515,14 +513,13 @@ golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
|
@ -535,8 +532,8 @@ golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
|||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk=
|
||||
google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis=
|
||||
|
|
@ -548,8 +545,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 h1:
|
|||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
@ -557,7 +554,6 @@ gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
|||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -240,6 +240,8 @@ const (
|
|||
CodespaceStateAvailable = "Available"
|
||||
// CodespaceStateShutdown is the state for a shutdown codespace environment.
|
||||
CodespaceStateShutdown = "Shutdown"
|
||||
// CodespaceStateShuttingDown is the state for a shutting down codespace environment.
|
||||
CodespaceStateShuttingDown = "ShuttingDown"
|
||||
// CodespaceStateStarting is the state for a starting codespace environment.
|
||||
CodespaceStateStarting = "Starting"
|
||||
// CodespaceStateRebuilding is the state for a rebuilding codespace environment.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,15 @@ import (
|
|||
"github.com/cli/cli/v2/internal/codespaces/connection"
|
||||
)
|
||||
|
||||
// codespaceStatePollingBackoff is the delay between state polls while waiting for codespaces to become
|
||||
// available. It's only exposed so that it can be shortened for testing, otherwise it should not be changed
|
||||
var codespaceStatePollingBackoff backoff.BackOff = backoff.NewExponentialBackOff(
|
||||
backoff.WithInitialInterval(1*time.Second),
|
||||
backoff.WithMultiplier(1.02),
|
||||
backoff.WithMaxInterval(10*time.Second),
|
||||
backoff.WithMaxElapsedTime(5*time.Minute),
|
||||
)
|
||||
|
||||
func connectionReady(codespace *api.Codespace) bool {
|
||||
// If the codespace is not available, it is not ready
|
||||
if codespace.State != api.CodespaceStateAvailable {
|
||||
|
|
@ -67,41 +76,53 @@ func GetCodespaceConnection(ctx context.Context, progress progressIndicator, api
|
|||
|
||||
// waitUntilCodespaceConnectionReady waits for a Codespace to be running and is able to be connected to.
|
||||
func waitUntilCodespaceConnectionReady(ctx context.Context, progress progressIndicator, apiClient apiClient, codespace *api.Codespace) (*api.Codespace, error) {
|
||||
if codespace.State != api.CodespaceStateAvailable {
|
||||
progress.StartProgressIndicatorWithLabel("Starting codespace")
|
||||
defer progress.StopProgressIndicator()
|
||||
if err := apiClient.StartCodespace(ctx, codespace.Name); err != nil {
|
||||
return nil, fmt.Errorf("error starting codespace: %w", err)
|
||||
}
|
||||
if connectionReady(codespace) {
|
||||
return codespace, nil
|
||||
}
|
||||
|
||||
if !connectionReady(codespace) {
|
||||
expBackoff := backoff.NewExponentialBackOff()
|
||||
expBackoff.Multiplier = 1.1
|
||||
expBackoff.MaxInterval = 10 * time.Second
|
||||
expBackoff.MaxElapsedTime = 5 * time.Minute
|
||||
progress.StartProgressIndicatorWithLabel("Waiting for codespace to become ready")
|
||||
defer progress.StopProgressIndicator()
|
||||
|
||||
err := backoff.Retry(func() error {
|
||||
var err error
|
||||
lastState := ""
|
||||
firstRetry := true
|
||||
|
||||
err := backoff.Retry(func() error {
|
||||
var err error
|
||||
if firstRetry {
|
||||
firstRetry = false
|
||||
} else {
|
||||
codespace, err = apiClient.GetCodespace(ctx, codespace.Name, true)
|
||||
if err != nil {
|
||||
return backoff.Permanent(fmt.Errorf("error getting codespace: %w", err))
|
||||
}
|
||||
|
||||
if connectionReady(codespace) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &TimeoutError{message: "codespace not ready yet"}
|
||||
}, backoff.WithContext(expBackoff, ctx))
|
||||
if err != nil {
|
||||
var timeoutErr *TimeoutError
|
||||
if errors.As(err, &timeoutErr) {
|
||||
return nil, errors.New("timed out while waiting for the codespace to start")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if connectionReady(codespace) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only react to changes in the state (so that we don't try to start the codespace twice)
|
||||
if codespace.State != lastState {
|
||||
if codespace.State == api.CodespaceStateShutdown {
|
||||
err = apiClient.StartCodespace(ctx, codespace.Name)
|
||||
if err != nil {
|
||||
return backoff.Permanent(fmt.Errorf("error starting codespace: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastState = codespace.State
|
||||
|
||||
return &TimeoutError{message: "codespace not ready yet"}
|
||||
}, backoff.WithContext(codespaceStatePollingBackoff, ctx))
|
||||
|
||||
if err != nil {
|
||||
var timeoutErr *TimeoutError
|
||||
if errors.As(err, &timeoutErr) {
|
||||
return nil, errors.New("timed out while waiting for the codespace to start")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return codespace, nil
|
||||
|
|
|
|||
212
internal/codespaces/codespaces_test.go
Normal file
212
internal/codespaces/codespaces_test.go
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
package codespaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Set the backoff to 0 for testing so that they run quickly
|
||||
codespaceStatePollingBackoff = backoff.NewConstantBackOff(time.Second * 0)
|
||||
}
|
||||
|
||||
// This is just enough to trick `connectionReady`
|
||||
var readyCodespace = &api.Codespace{
|
||||
State: api.CodespaceStateAvailable,
|
||||
Connection: api.CodespaceConnection{
|
||||
TunnelProperties: api.TunnelProperties{
|
||||
ConnectAccessToken: "test",
|
||||
ManagePortsAccessToken: "test",
|
||||
ServiceUri: "test",
|
||||
TunnelId: "test",
|
||||
ClusterId: "test",
|
||||
Domain: "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestWaitUntilCodespaceConnectionReady_WhenAlreadyReady(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
apiClient := &mockApiClient{}
|
||||
result, err := waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, readyCodespace)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected nil error, but was %v", err)
|
||||
}
|
||||
if result.State != api.CodespaceStateAvailable {
|
||||
t.Fatalf("Expected final state to be %s, but was %s", api.CodespaceStateAvailable, result.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitUntilCodespaceConnectionReady_PollsApi(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
apiClient := &mockApiClient{
|
||||
onGetCodespace: func() (*api.Codespace, error) {
|
||||
return readyCodespace, nil
|
||||
},
|
||||
}
|
||||
result, err := waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, &api.Codespace{State: api.CodespaceStateStarting})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected nil error, but was %v", err)
|
||||
}
|
||||
if result.State != api.CodespaceStateAvailable {
|
||||
t.Fatalf("Expected final state to be %s, but was %s", api.CodespaceStateAvailable, result.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitUntilCodespaceConnectionReady_StartsCodespace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
codespace := &api.Codespace{State: api.CodespaceStateShutdown}
|
||||
|
||||
apiClient := &mockApiClient{
|
||||
onGetCodespace: func() (*api.Codespace, error) {
|
||||
return codespace, nil
|
||||
},
|
||||
onStartCodespace: func() error {
|
||||
*codespace = *readyCodespace
|
||||
return nil
|
||||
},
|
||||
}
|
||||
result, err := waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected nil error, but was %v", err)
|
||||
}
|
||||
if result.State != api.CodespaceStateAvailable {
|
||||
t.Fatalf("Expected final state to be %s, but was %s", api.CodespaceStateAvailable, result.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitUntilCodespaceConnectionReady_PollsCodespaceUntilReady(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
codespace := &api.Codespace{State: api.CodespaceStateShutdown}
|
||||
hasPolled := false
|
||||
|
||||
apiClient := &mockApiClient{
|
||||
onGetCodespace: func() (*api.Codespace, error) {
|
||||
if hasPolled {
|
||||
*codespace = *readyCodespace
|
||||
}
|
||||
|
||||
hasPolled = true
|
||||
|
||||
return codespace, nil
|
||||
},
|
||||
onStartCodespace: func() error {
|
||||
codespace.State = api.CodespaceStateStarting
|
||||
return nil
|
||||
},
|
||||
}
|
||||
result, err := waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected nil error, but was %v", err)
|
||||
}
|
||||
if result.State != api.CodespaceStateAvailable {
|
||||
t.Fatalf("Expected final state to be %s, but was %s", api.CodespaceStateAvailable, result.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitUntilCodespaceConnectionReady_WaitsForShutdownBeforeStarting(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
codespace := &api.Codespace{State: api.CodespaceStateShuttingDown}
|
||||
|
||||
apiClient := &mockApiClient{
|
||||
onGetCodespace: func() (*api.Codespace, error) {
|
||||
// Make sure that we poll at least once before going to shutdown
|
||||
if codespace.State == api.CodespaceStateShuttingDown {
|
||||
codespace.State = api.CodespaceStateShutdown
|
||||
}
|
||||
return codespace, nil
|
||||
},
|
||||
onStartCodespace: func() error {
|
||||
if codespace.State != api.CodespaceStateShutdown {
|
||||
t.Fatalf("Codespace started from non-shutdown state: %s", codespace.State)
|
||||
}
|
||||
*codespace = *readyCodespace
|
||||
return nil
|
||||
},
|
||||
}
|
||||
result, err := waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected nil error, but was %v", err)
|
||||
}
|
||||
if result.State != api.CodespaceStateAvailable {
|
||||
t.Fatalf("Expected final state to be %s, but was %s", api.CodespaceStateAvailable, result.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUntilCodespaceConnectionReady_DoesntStartTwice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
codespace := &api.Codespace{State: api.CodespaceStateShutdown}
|
||||
didStart := false
|
||||
didPollAfterStart := false
|
||||
|
||||
apiClient := &mockApiClient{
|
||||
onGetCodespace: func() (*api.Codespace, error) {
|
||||
// Make sure that we are in shutdown state for one poll after starting to make sure we don't try to start again
|
||||
if didPollAfterStart {
|
||||
*codespace = *readyCodespace
|
||||
}
|
||||
|
||||
if didStart {
|
||||
didPollAfterStart = true
|
||||
}
|
||||
|
||||
return codespace, nil
|
||||
},
|
||||
onStartCodespace: func() error {
|
||||
if didStart {
|
||||
t.Fatal("Should not start multiple times")
|
||||
}
|
||||
didStart = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
result, err := waitUntilCodespaceConnectionReady(context.Background(), &mockProgressIndicator{}, apiClient, codespace)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected nil error, but was %v", err)
|
||||
}
|
||||
if result.State != api.CodespaceStateAvailable {
|
||||
t.Fatalf("Expected final state to be %s, but was %s", api.CodespaceStateAvailable, result.State)
|
||||
}
|
||||
}
|
||||
|
||||
type mockApiClient struct {
|
||||
onStartCodespace func() error
|
||||
onGetCodespace func() (*api.Codespace, error)
|
||||
}
|
||||
|
||||
func (m *mockApiClient) StartCodespace(ctx context.Context, name string) error {
|
||||
if m.onStartCodespace == nil {
|
||||
panic("onStartCodespace not set and StartCodespace was called")
|
||||
}
|
||||
|
||||
return m.onStartCodespace()
|
||||
}
|
||||
|
||||
func (m *mockApiClient) GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
|
||||
if m.onGetCodespace == nil {
|
||||
panic("onGetCodespace not set and GetCodespace was called")
|
||||
}
|
||||
|
||||
return m.onGetCodespace()
|
||||
}
|
||||
|
||||
func (m *mockApiClient) HTTPClient() (*http.Client, error) {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
type mockProgressIndicator struct{}
|
||||
|
||||
func (m *mockProgressIndicator) StartProgressIndicatorWithLabel(s string) {}
|
||||
func (m *mockProgressIndicator) StopProgressIndicator() {}
|
||||
|
|
@ -90,7 +90,7 @@ type Migration interface {
|
|||
}
|
||||
|
||||
// AuthConfig is used for interacting with some persistent configuration for gh,
|
||||
// with knowledge on how to access encrypted storage when neccesarry.
|
||||
// with knowledge on how to access encrypted storage when necessary.
|
||||
// Behavior is scoped to authentication specific tasks.
|
||||
type AuthConfig interface {
|
||||
// HasActiveToken returns true when a token for the hostname is present.
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package io
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
)
|
||||
|
|
@ -65,26 +65,24 @@ func (h *Handler) VerbosePrintf(f string, v ...interface{}) (int, error) {
|
|||
if !h.debugEnabled || !h.IO.IsStdoutTTY() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return fmt.Fprintf(h.IO.ErrOut, f, v...)
|
||||
}
|
||||
|
||||
func (h *Handler) PrintTable(headers []string, rows [][]string) error {
|
||||
func (h *Handler) PrintBulletPoints(rows [][]string) (int, error) {
|
||||
if !h.IO.IsStdoutTTY() {
|
||||
return nil
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
t := tableprinter.New(h.IO, tableprinter.WithHeader(headers...))
|
||||
|
||||
maxColLen := 0
|
||||
for _, row := range rows {
|
||||
for _, field := range row {
|
||||
t.AddField(field, tableprinter.WithTruncate(nil))
|
||||
if len(row[0]) > maxColLen {
|
||||
maxColLen = len(row[0])
|
||||
}
|
||||
t.EndRow()
|
||||
}
|
||||
|
||||
if err := t.Render(); err != nil {
|
||||
return fmt.Errorf("failed to print output: %v", err)
|
||||
info := ""
|
||||
for _, row := range rows {
|
||||
dots := strings.Repeat(".", maxColLen-len(row[0]))
|
||||
info += fmt.Sprintf("%s:%s %s\n", row[0], dots, row[1])
|
||||
}
|
||||
return nil
|
||||
return fmt.Fprintln(h.IO.ErrOut, info)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ func (v *FailSigstoreVerifier) Verify([]*api.Attestation, verify.PolicyBuilder)
|
|||
return nil, fmt.Errorf("failed to verify attestations")
|
||||
}
|
||||
|
||||
func BuildMockResult(b *bundle.Bundle, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer string) AttestationProcessingResult {
|
||||
func BuildMockResult(b *bundle.Bundle, buildConfigURI, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer string) AttestationProcessingResult {
|
||||
statement := &in_toto.Statement{}
|
||||
statement.PredicateType = SLSAPredicateV1
|
||||
|
||||
|
|
@ -80,10 +80,11 @@ func BuildMockResult(b *bundle.Bundle, buildSignerURI, sourceRepoOwnerURI, sourc
|
|||
Signature: &verify.SignatureVerificationResult{
|
||||
Certificate: &certificate.Summary{
|
||||
Extensions: certificate.Extensions{
|
||||
BuildConfigURI: buildConfigURI,
|
||||
BuildSignerURI: buildSignerURI,
|
||||
Issuer: issuer,
|
||||
SourceRepositoryOwnerURI: sourceRepoOwnerURI,
|
||||
SourceRepositoryURI: sourceRepoURI,
|
||||
Issuer: issuer,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -93,9 +94,10 @@ func BuildMockResult(b *bundle.Bundle, buildSignerURI, sourceRepoOwnerURI, sourc
|
|||
|
||||
func BuildSigstoreJsMockResult(t *testing.T) AttestationProcessingResult {
|
||||
bundle := data.SigstoreBundle(t)
|
||||
buildConfigURI := "https://github.com/sigstore/sigstore-js/.github/workflows/build.yml@refs/heads/main"
|
||||
buildSignerURI := "https://github.com/github/example/.github/workflows/release.yml@refs/heads/main"
|
||||
sourceRepoOwnerURI := "https://github.com/sigstore"
|
||||
sourceRepoURI := "https://github.com/sigstore/sigstore-js"
|
||||
issuer := "https://token.actions.githubusercontent.com"
|
||||
return BuildMockResult(bundle, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer)
|
||||
return BuildMockResult(bundle, buildConfigURI, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,10 +54,7 @@ func (c EnforcementCriteria) Valid() error {
|
|||
func (c EnforcementCriteria) BuildPolicyInformation() string {
|
||||
policyAttr := make([][]string, 0, 6)
|
||||
|
||||
policyAttr = appendStr(policyAttr, "- OIDC Issuer must match", c.Certificate.Issuer)
|
||||
if c.Certificate.RunnerEnvironment == GitHubRunner {
|
||||
policyAttr = appendStr(policyAttr, "- Action workflow Runner Environment must match ", GitHubRunner)
|
||||
}
|
||||
policyAttr = appendStr(policyAttr, "- Predicate type must match", c.PredicateType)
|
||||
|
||||
policyAttr = appendStr(policyAttr, "- Source Repository Owner URI must match", c.Certificate.SourceRepositoryOwnerURI)
|
||||
|
||||
|
|
@ -65,14 +62,17 @@ func (c EnforcementCriteria) BuildPolicyInformation() string {
|
|||
policyAttr = appendStr(policyAttr, "- Source Repository URI must match", c.Certificate.SourceRepositoryURI)
|
||||
}
|
||||
|
||||
policyAttr = appendStr(policyAttr, "- Predicate type must match", c.PredicateType)
|
||||
|
||||
if c.SAN != "" {
|
||||
policyAttr = appendStr(policyAttr, "- Subject Alternative Name must match", c.SAN)
|
||||
} else if c.SANRegex != "" {
|
||||
policyAttr = appendStr(policyAttr, "- Subject Alternative Name must match regex", c.SANRegex)
|
||||
}
|
||||
|
||||
policyAttr = appendStr(policyAttr, "- OIDC Issuer must match", c.Certificate.Issuer)
|
||||
if c.Certificate.RunnerEnvironment == GitHubRunner {
|
||||
policyAttr = appendStr(policyAttr, "- Action workflow Runner Environment must match ", GitHubRunner)
|
||||
}
|
||||
|
||||
maxColLen := 0
|
||||
for _, attr := range policyAttr {
|
||||
if len(attr[0]) > maxColLen {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ func TestVerifyAttestations(t *testing.T) {
|
|||
attestations := []*api.Attestation{sgjAttestation[0], reusableWorkflowAttestations[0], sgjAttestation[1]}
|
||||
require.Len(t, attestations, 3)
|
||||
|
||||
rwfResult := verification.BuildMockResult(reusableWorkflowAttestations[0].Bundle, "", "https://github.com/malancas", "", verification.GitHubOIDCIssuer)
|
||||
rwfResult := verification.BuildMockResult(reusableWorkflowAttestations[0].Bundle, "", "", "https://github.com/malancas", "", verification.GitHubOIDCIssuer)
|
||||
sgjResult := verification.BuildSigstoreJsMockResult(t)
|
||||
mockResults := []*verification.AttestationProcessingResult{&sgjResult, &rwfResult, &sgjResult}
|
||||
mockSgVerifier := verification.NewMockSigstoreVerifierWithMockResults(t, mockResults)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ func newEnforcementCriteria(opts *Options) (verification.EnforcementCriteria, er
|
|||
signedRepoRegex := expandToGitHubURLRegex(opts.Tenant, opts.SignerRepo)
|
||||
c.SANRegex = signedRepoRegex
|
||||
} else if opts.SignerWorkflow != "" {
|
||||
validatedWorkflowRegex, err := validateSignerWorkflow(opts)
|
||||
validatedWorkflowRegex, err := validateSignerWorkflow(opts.Hostname, opts.SignerWorkflow)
|
||||
if err != nil {
|
||||
return verification.EnforcementCriteria{}, err
|
||||
}
|
||||
|
|
@ -140,23 +140,23 @@ func buildSigstoreVerifyPolicy(c verification.EnforcementCriteria, a artifact.Di
|
|||
return policy, nil
|
||||
}
|
||||
|
||||
func validateSignerWorkflow(opts *Options) (string, error) {
|
||||
func validateSignerWorkflow(hostname, signerWorkflow string) (string, error) {
|
||||
// we expect a provided workflow argument be in the format [HOST/]/<OWNER>/<REPO>/path/to/workflow.yml
|
||||
// if the provided workflow does not contain a host, set the host
|
||||
match, err := regexp.MatchString(hostRegex, opts.SignerWorkflow)
|
||||
match, err := regexp.MatchString(hostRegex, signerWorkflow)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if match {
|
||||
return fmt.Sprintf("^https://%s", opts.SignerWorkflow), nil
|
||||
return fmt.Sprintf("^https://%s", signerWorkflow), nil
|
||||
}
|
||||
|
||||
// if the provided workflow did not match the expect format
|
||||
// we move onto creating a signer workflow using the provided host name
|
||||
if opts.Hostname == "" {
|
||||
if hostname == "" {
|
||||
return "", errors.New("unknown host")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("^https://%s/%s", opts.Hostname, opts.SignerWorkflow), nil
|
||||
return fmt.Sprintf("^https://%s/%s", hostname, signerWorkflow), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
"github.com/cli/cli/v2/pkg/cmd/factory"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -263,14 +262,8 @@ func TestValidateSignerWorkflow(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
opts := &Options{
|
||||
Config: factory.New("test").Config,
|
||||
SignerWorkflow: tc.providedSignerWorkflow,
|
||||
}
|
||||
|
||||
// All host resolution is done verify.go:RunE
|
||||
opts.Hostname = tc.host
|
||||
workflowRegex, err := validateSignerWorkflow(opts)
|
||||
workflowRegex, err := validateSignerWorkflow(tc.host, tc.providedSignerWorkflow)
|
||||
require.Equal(t, tc.expectedWorkflowRegex, workflowRegex)
|
||||
|
||||
if tc.expectErr {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"regexp"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
|
||||
|
|
@ -261,19 +262,32 @@ func runVerify(opts *Options) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
opts.Logger.Printf("%s was attested by:\n", artifact.DigestWithAlg())
|
||||
opts.Logger.Printf("The following %s matched the policy criteria\n\n", text.Pluralize(len(verified), "attestation"))
|
||||
|
||||
// Otherwise print the results to the terminal in a table
|
||||
tableContent, err := buildTableVerifyContent(opts.Tenant, verified)
|
||||
if err != nil {
|
||||
opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse results"))
|
||||
return err
|
||||
}
|
||||
// Otherwise print the results to the terminal
|
||||
for i, v := range verified {
|
||||
buildConfigURI := v.VerificationResult.Signature.Certificate.Extensions.BuildConfigURI
|
||||
sourceRepoAndOrg, sourceWorkflow, err := extractAttestationDetail(opts.Tenant, buildConfigURI)
|
||||
if err != nil {
|
||||
opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse build config URI"))
|
||||
return err
|
||||
}
|
||||
builderSignerURI := v.VerificationResult.Signature.Certificate.Extensions.BuildSignerURI
|
||||
signerRepoAndOrg, signerWorkflow, err := extractAttestationDetail(opts.Tenant, builderSignerURI)
|
||||
if err != nil {
|
||||
opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse build signer URI"))
|
||||
return err
|
||||
}
|
||||
|
||||
headers := []string{"repo", "predicate_type", "workflow"}
|
||||
if err = opts.Logger.PrintTable(headers, tableContent); err != nil {
|
||||
opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to print attestation details to table"))
|
||||
return err
|
||||
opts.Logger.Printf("- Attestation #%d\n", i+1)
|
||||
rows := [][]string{
|
||||
{" - Build repo", sourceRepoAndOrg},
|
||||
{" - Build workflow", sourceWorkflow},
|
||||
{" - Signer repo", signerRepoAndOrg},
|
||||
{" - Signer workflow", signerWorkflow},
|
||||
}
|
||||
//nolint:errcheck
|
||||
opts.Logger.PrintBulletPoints(rows)
|
||||
}
|
||||
|
||||
// All attestations passed verification and policy evaluation
|
||||
|
|
@ -304,39 +318,15 @@ func extractAttestationDetail(tenant, builderSignerURI string) (string, string,
|
|||
|
||||
match := orgAndRepoRegexp.FindStringSubmatch(builderSignerURI)
|
||||
if len(match) < 2 {
|
||||
return "", "", fmt.Errorf("no match found for org and repo")
|
||||
return "", "", fmt.Errorf("no match found for org and repo: %s", builderSignerURI)
|
||||
}
|
||||
orgAndRepo := match[1]
|
||||
|
||||
match = workflowRegexp.FindStringSubmatch(builderSignerURI)
|
||||
if len(match) < 2 {
|
||||
return "", "", fmt.Errorf("no match found for workflow")
|
||||
return "", "", fmt.Errorf("no match found for workflow: %s", builderSignerURI)
|
||||
}
|
||||
workflow := match[1]
|
||||
|
||||
return orgAndRepo, workflow, nil
|
||||
}
|
||||
|
||||
func buildTableVerifyContent(tenant string, results []*verification.AttestationProcessingResult) ([][]string, error) {
|
||||
content := make([][]string, len(results))
|
||||
|
||||
for i, res := range results {
|
||||
if res.VerificationResult == nil ||
|
||||
res.VerificationResult.Signature == nil ||
|
||||
res.VerificationResult.Signature.Certificate == nil {
|
||||
return nil, fmt.Errorf("bundle missing verification result fields")
|
||||
}
|
||||
builderSignerURI := res.VerificationResult.Signature.Certificate.Extensions.BuildSignerURI
|
||||
repoAndOrg, workflow, err := extractAttestationDetail(tenant, builderSignerURI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.VerificationResult.Statement == nil {
|
||||
return nil, fmt.Errorf("bundle missing attestation statement (bundle must originate from GitHub Artifact Attestations)")
|
||||
}
|
||||
predicateType := res.VerificationResult.Statement.PredicateType
|
||||
content[i] = []string{repoAndOrg, predicateType, workflow}
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ func TestRunVerify(t *testing.T) {
|
|||
opts.BundlePath = ""
|
||||
opts.Owner = "sigstore"
|
||||
|
||||
require.Nil(t, runVerify(&opts))
|
||||
require.NoError(t, runVerify(&opts))
|
||||
})
|
||||
|
||||
t.Run("with owner which not matches SourceRepositoryOwnerURI", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package token
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
|
|
@ -88,7 +89,7 @@ func tokenRun(opts *TokenOptions) error {
|
|||
if opts.Username != "" {
|
||||
errMsg += fmt.Sprintf(" account %s", opts.Username)
|
||||
}
|
||||
return fmt.Errorf(errMsg)
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
|
||||
if val != "" {
|
||||
|
|
|
|||
|
|
@ -648,14 +648,14 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
|
|||
assert.EqualError(t, err, tt.wantErr.Error())
|
||||
}
|
||||
if err != nil && tt.wantErr == nil {
|
||||
t.Logf(err.Error())
|
||||
t.Log(err.Error())
|
||||
}
|
||||
if got := stdout.String(); got != tt.wantStdout {
|
||||
t.Logf(t.Name())
|
||||
t.Log(t.Name())
|
||||
t.Errorf(" stdout = %v, want %v", got, tt.wantStdout)
|
||||
}
|
||||
if got := stderr.String(); got != tt.wantStderr {
|
||||
t.Logf(t.Name())
|
||||
t.Log(t.Name())
|
||||
t.Errorf(" stderr = %v, want %v", got, tt.wantStderr)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ func TestGenerateAutomaticSSHKeys(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSelectSSHKeys(t *testing.T) {
|
||||
// This string will be subsituted in sshArgs for test cases
|
||||
// This string will be substituted in sshArgs for test cases
|
||||
// This is to work around the temp test ssh dir not being known until the test is executing
|
||||
substituteSSHDir := "SUB_SSH_DIR"
|
||||
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ func BaseRepoFunc(f *cmdutil.Factory) func() (ghrepo.Interface, error) {
|
|||
// even more confusion because the API requests can be different, and FURTHERMORE this can be an issue for
|
||||
// services that don't handle renames correctly, like the ElasticSearch indexing.
|
||||
//
|
||||
// Assuming we have an interactive invocation, then the next step is to resolve a network of respositories. This
|
||||
// Assuming we have an interactive invocation, then the next step is to resolve a network of repositories. This
|
||||
// involves creating a dynamic GQL query requesting information about each repository (up to a limit of 5).
|
||||
// Each returned repo is added to a list, along with its parent, if present in the query response.
|
||||
// The repositories in the query retain the same ordering as previously outlined. Interestingly, the request is sent
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ func (rr *remoteResolver) Resolver() func() (context.Remotes, error) {
|
|||
sort.Sort(resolvedRemotes)
|
||||
|
||||
// Filter remotes by hosts
|
||||
// Note that this is not caching correctly: https://github.com/cli/cli/issues/10103
|
||||
cachedRemotes := resolvedRemotes.FilterByHosts(hosts)
|
||||
|
||||
// Filter again by default host if one is set
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -18,8 +20,10 @@ type DeleteOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
Config func() (gh.Config, error)
|
||||
HttpClient func() (*http.Client, error)
|
||||
Prompter prompter.Prompter
|
||||
|
||||
Selector string
|
||||
Selector string
|
||||
Confirmed bool
|
||||
}
|
||||
|
||||
func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
|
||||
|
|
@ -27,33 +31,51 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
IO: f.IOStreams,
|
||||
Config: f.Config,
|
||||
HttpClient: f.HttpClient,
|
||||
Prompter: f.Prompter,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete {<id> | <url>}",
|
||||
Short: "Delete a gist",
|
||||
Args: cmdutil.ExactArgs(1, "cannot delete: gist argument required"),
|
||||
Long: heredoc.Docf(`
|
||||
Delete a GitHub gist.
|
||||
|
||||
To delete a gist interactively, use %[1]sgh gist delete%[1]s with no arguments.
|
||||
|
||||
To delete a gist non-interactively, supply the gist id or url.
|
||||
`, "`"),
|
||||
Example: heredoc.Doc(`
|
||||
# delete a gist interactively
|
||||
gh gist delete
|
||||
|
||||
# delete a gist non-interactively
|
||||
gh gist delete 1234
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
opts.Selector = args[0]
|
||||
if !opts.IO.CanPrompt() && !opts.Confirmed {
|
||||
return cmdutil.FlagErrorf("--yes required when not running interactively")
|
||||
}
|
||||
|
||||
if !opts.IO.CanPrompt() && len(args) == 0 {
|
||||
return cmdutil.FlagErrorf("id or url argument required in non-interactive mode")
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
opts.Selector = args[0]
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(&opts)
|
||||
}
|
||||
return deleteRun(&opts)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&opts.Confirmed, "yes", false, "confirm deletion without prompting")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func deleteRun(opts *DeleteOptions) error {
|
||||
gistID := opts.Selector
|
||||
|
||||
if strings.Contains(gistID, "/") {
|
||||
id, err := shared.GistIDFromURL(gistID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gistID = id
|
||||
}
|
||||
client, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -66,14 +88,56 @@ func deleteRun(opts *DeleteOptions) error {
|
|||
|
||||
host, _ := cfg.Authentication().DefaultHost()
|
||||
|
||||
gistID := opts.Selector
|
||||
if strings.Contains(gistID, "/") {
|
||||
id, err := shared.GistIDFromURL(gistID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gistID = id
|
||||
}
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
var gist *shared.Gist
|
||||
if gistID == "" {
|
||||
gist, err = shared.PromptGists(opts.Prompter, client, host, cs)
|
||||
} else {
|
||||
gist, err = shared.GetGist(client, host, gistID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if gist.ID == "" {
|
||||
fmt.Fprintln(opts.IO.Out, "No gists found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !opts.Confirmed {
|
||||
confirmed, err := opts.Prompter.Confirm(fmt.Sprintf("Delete %q gist?", gist.Filename()), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirmed {
|
||||
return cmdutil.CancelError
|
||||
}
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
if err := deleteGist(apiClient, host, gistID); err != nil {
|
||||
if err := deleteGist(apiClient, host, gist.ID); err != nil {
|
||||
if errors.Is(err, shared.NotFoundErr) {
|
||||
return fmt.Errorf("unable to delete gist %s: either the gist is not found or it is not owned by you", gistID)
|
||||
return fmt.Errorf("unable to delete gist %q: either the gist is not found or it is not owned by you", gist.Filename())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.Out,
|
||||
"%s Gist %q deleted\n",
|
||||
cs.SuccessIcon(),
|
||||
gist.Filename())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,36 +2,95 @@ package delete
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
ghAPI "github.com/cli/go-gh/v2/pkg/api"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants DeleteOptions
|
||||
name string
|
||||
cli string
|
||||
tty bool
|
||||
want DeleteOptions
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid selector",
|
||||
cli: "123",
|
||||
wants: DeleteOptions{
|
||||
tty: true,
|
||||
want: DeleteOptions{
|
||||
Selector: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid selector, no ID supplied",
|
||||
cli: "",
|
||||
tty: true,
|
||||
want: DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no ID supplied with --yes",
|
||||
cli: "--yes",
|
||||
tty: true,
|
||||
want: DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "selector with --yes, no tty",
|
||||
cli: "123 --yes",
|
||||
tty: false,
|
||||
want: DeleteOptions{
|
||||
Selector: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ID arg without --yes, no tty",
|
||||
cli: "123",
|
||||
tty: false,
|
||||
want: DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "--yes required when not running interactively",
|
||||
},
|
||||
{
|
||||
name: "no ID supplied with --yes, no tty",
|
||||
cli: "--yes",
|
||||
tty: false,
|
||||
want: DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "id or url argument required in non-interactive mode",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := &cmdutil.Factory{}
|
||||
io, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
}
|
||||
io.SetStdinTTY(tt.tty)
|
||||
io.SetStdoutTTY(tt.tty)
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -47,69 +106,210 @@ func TestNewCmdDelete(t *testing.T) {
|
|||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
assert.NoError(t, err)
|
||||
if tt.wantErr {
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wants.Selector, gotOpts.Selector)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want.Selector, gotOpts.Selector)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_deleteRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts DeleteOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
wantErr bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
name string
|
||||
opts *DeleteOptions
|
||||
cancel bool
|
||||
httpStubs func(*httpmock.Registry)
|
||||
mockPromptGists bool
|
||||
noGists bool
|
||||
wantErr bool
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
}{
|
||||
{
|
||||
name: "successfully delete",
|
||||
opts: DeleteOptions{
|
||||
opts: &DeleteOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
|
||||
reg.Register(httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(200, "{}"))
|
||||
},
|
||||
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
|
||||
},
|
||||
{
|
||||
name: "successfully delete with prompt",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(200, "{}"))
|
||||
},
|
||||
wantErr: false,
|
||||
wantStdout: "",
|
||||
wantStderr: "",
|
||||
mockPromptGists: true,
|
||||
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
opts: DeleteOptions{
|
||||
name: "successfully delete with --yes",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "1234",
|
||||
Confirmed: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
|
||||
reg.Register(httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(200, "{}"))
|
||||
},
|
||||
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
|
||||
},
|
||||
{
|
||||
name: "successfully delete with prompt and --yes",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "",
|
||||
Confirmed: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(200, "{}"))
|
||||
},
|
||||
mockPromptGists: true,
|
||||
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
|
||||
},
|
||||
{
|
||||
name: "cancel delete with id",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
|
||||
},
|
||||
cancel: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "cancel delete with url",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "https://gist.github.com/myrepo/1234",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
|
||||
},
|
||||
cancel: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "cancel delete with prompt",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {},
|
||||
mockPromptGists: true,
|
||||
cancel: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "not owned by you",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
|
||||
reg.Register(httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(404, "{}"))
|
||||
},
|
||||
wantErr: true,
|
||||
wantStdout: "",
|
||||
wantStderr: "",
|
||||
wantStderr: "unable to delete gist \"cool.txt\": either the gist is not found or it is not owned by you",
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.StatusStringResponse(404, "{}"))
|
||||
},
|
||||
wantErr: true,
|
||||
wantStderr: "not found",
|
||||
},
|
||||
{
|
||||
name: "no gists",
|
||||
opts: &DeleteOptions{
|
||||
Selector: "",
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {},
|
||||
mockPromptGists: true,
|
||||
noGists: true,
|
||||
wantStdout: "No gists found.\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg)
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
if !tt.opts.Confirmed {
|
||||
pm.RegisterConfirm("Delete \"cool.txt\" gist?", func(_ string, _ bool) (bool, error) {
|
||||
return !tt.cancel, nil
|
||||
})
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
tt.httpStubs(reg)
|
||||
if tt.mockPromptGists {
|
||||
if tt.noGists {
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query GistList\b`),
|
||||
httpmock.StringResponse(
|
||||
`{ "data": { "viewer": { "gists": { "nodes": [] }} } }`),
|
||||
)
|
||||
} else {
|
||||
sixHours, _ := time.ParseDuration("6h")
|
||||
sixHoursAgo := time.Now().Add(-sixHours)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`query GistList\b`),
|
||||
httpmock.StringResponse(fmt.Sprintf(
|
||||
`{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "1234",
|
||||
"files": [{ "name": "cool.txt" }],
|
||||
"updatedAt": "%s",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
sixHoursAgo.Format(time.RFC3339),
|
||||
)),
|
||||
)
|
||||
pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, _ []string) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tt.opts.Prompter = pm
|
||||
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
||||
tt.opts.Config = func() (gh.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
}
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(false)
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStdinTTY(false)
|
||||
tt.opts.IO = ios
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := deleteRun(&tt.opts)
|
||||
err := deleteRun(tt.opts)
|
||||
reg.Verify(t)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
|
|
@ -119,6 +319,75 @@ func Test_deleteRun(t *testing.T) {
|
|||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_gistDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
httpStubs func(*httpmock.Registry)
|
||||
hostname string
|
||||
gistID string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "successful delete",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(204, "{}"),
|
||||
)
|
||||
},
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "when an gist is not found, it returns a NotFoundError",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusStringResponse(404, "{}"),
|
||||
)
|
||||
},
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErr: shared.NotFoundErr,
|
||||
},
|
||||
{
|
||||
name: "when there is a non-404 error deleting the gist, that error is returned",
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("DELETE", "gists/1234"),
|
||||
httpmock.StatusJSONResponse(500, `{"message": "arbitrary error"}`),
|
||||
)
|
||||
},
|
||||
hostname: "github.com",
|
||||
gistID: "1234",
|
||||
wantErr: api.HTTPError{
|
||||
HTTPError: &ghAPI.HTTPError{
|
||||
StatusCode: 500,
|
||||
Message: "arbitrary error",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
tt.httpStubs(reg)
|
||||
client := api.NewClientFromHTTP(&http.Client{Transport: reg})
|
||||
|
||||
err := deleteGist(client, tt.hostname, tt.gistID)
|
||||
if tt.wantErr != nil {
|
||||
assert.ErrorAs(t, err, &tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,15 +108,20 @@ func editRun(opts *EditOptions) error {
|
|||
if gistID == "" {
|
||||
cs := opts.IO.ColorScheme()
|
||||
if gistID == "" {
|
||||
gistID, err = shared.PromptGists(opts.Prompter, client, host, cs)
|
||||
if !opts.IO.CanPrompt() {
|
||||
return cmdutil.FlagErrorf("gist ID or URL required when not running interactively")
|
||||
}
|
||||
|
||||
gist, err := shared.PromptGists(opts.Prompter, client, host, cs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gistID == "" {
|
||||
if gist.ID == "" {
|
||||
fmt.Fprintln(opts.IO.Out, "No gists found.")
|
||||
return nil
|
||||
}
|
||||
gistID = gist.ID
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package edit
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
|
|
@ -141,23 +143,31 @@ func Test_editRun(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *EditOptions
|
||||
gist *shared.Gist
|
||||
httpStubs func(*httpmock.Registry)
|
||||
prompterStubs func(*prompter.MockPrompter)
|
||||
nontty bool
|
||||
stdin string
|
||||
wantErr string
|
||||
wantParams map[string]interface{}
|
||||
name string
|
||||
opts *EditOptions
|
||||
mockGist *shared.Gist
|
||||
mockGistList bool
|
||||
httpStubs func(*httpmock.Registry)
|
||||
prompterStubs func(*prompter.MockPrompter)
|
||||
isTTY bool
|
||||
stdin string
|
||||
wantErr string
|
||||
wantLastRequestParameters map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "no such gist",
|
||||
wantErr: "gist not found: 1234",
|
||||
opts: &EditOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one file",
|
||||
gist: &shared.Gist{
|
||||
name: "one file",
|
||||
isTTY: false,
|
||||
opts: &EditOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
|
|
@ -172,7 +182,7 @@ func Test_editRun(t *testing.T) {
|
|||
reg.Register(httpmock.REST("POST", "gists/1234"),
|
||||
httpmock.StatusStringResponse(201, "{}"))
|
||||
},
|
||||
wantParams: map[string]interface{}{
|
||||
wantLastRequestParameters: map[string]interface{}{
|
||||
"description": "",
|
||||
"files": map[string]interface{}{
|
||||
"cicada.txt": map[string]interface{}{
|
||||
|
|
@ -183,7 +193,9 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "multiple files, submit",
|
||||
name: "multiple files, submit, with TTY",
|
||||
isTTY: true,
|
||||
mockGistList: true,
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Edit which file?",
|
||||
[]string{"cicada.txt", "unix.md"},
|
||||
|
|
@ -196,7 +208,7 @@ func Test_editRun(t *testing.T) {
|
|||
return prompter.IndexFor(opts, "Submit")
|
||||
})
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Description: "catbug",
|
||||
Files: map[string]*shared.GistFile{
|
||||
|
|
@ -215,7 +227,7 @@ func Test_editRun(t *testing.T) {
|
|||
reg.Register(httpmock.REST("POST", "gists/1234"),
|
||||
httpmock.StatusStringResponse(201, "{}"))
|
||||
},
|
||||
wantParams: map[string]interface{}{
|
||||
wantLastRequestParameters: map[string]interface{}{
|
||||
"description": "catbug",
|
||||
"files": map[string]interface{}{
|
||||
"cicada.txt": map[string]interface{}{
|
||||
|
|
@ -230,7 +242,11 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "multiple files, cancel",
|
||||
name: "multiple files, cancel, with TTY",
|
||||
isTTY: true,
|
||||
opts: &EditOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Edit which file?",
|
||||
[]string{"cicada.txt", "unix.md"},
|
||||
|
|
@ -244,7 +260,7 @@ func Test_editRun(t *testing.T) {
|
|||
})
|
||||
},
|
||||
wantErr: "CancelError",
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
|
|
@ -263,7 +279,10 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "not change",
|
||||
gist: &shared.Gist{
|
||||
opts: &EditOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
|
|
@ -277,7 +296,10 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "another user's gist",
|
||||
gist: &shared.Gist{
|
||||
opts: &EditOptions{
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
|
|
@ -292,7 +314,11 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "add file to existing gist",
|
||||
gist: &shared.Gist{
|
||||
opts: &EditOptions{
|
||||
AddFilename: fileToAdd,
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"sample.txt": {
|
||||
|
|
@ -307,16 +333,14 @@ func Test_editRun(t *testing.T) {
|
|||
reg.Register(httpmock.REST("POST", "gists/1234"),
|
||||
httpmock.StatusStringResponse(201, "{}"))
|
||||
},
|
||||
opts: &EditOptions{
|
||||
AddFilename: fileToAdd,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change description",
|
||||
opts: &EditOptions{
|
||||
Description: "my new description",
|
||||
Selector: "1234",
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Description: "my old description",
|
||||
Files: map[string]*shared.GistFile{
|
||||
|
|
@ -331,7 +355,7 @@ func Test_editRun(t *testing.T) {
|
|||
reg.Register(httpmock.REST("POST", "gists/1234"),
|
||||
httpmock.StatusStringResponse(201, "{}"))
|
||||
},
|
||||
wantParams: map[string]interface{}{
|
||||
wantLastRequestParameters: map[string]interface{}{
|
||||
"description": "my new description",
|
||||
"files": map[string]interface{}{
|
||||
"sample.txt": map[string]interface{}{
|
||||
|
|
@ -343,7 +367,12 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "add file to existing gist from source parameter",
|
||||
gist: &shared.Gist{
|
||||
opts: &EditOptions{
|
||||
AddFilename: "from_source.txt",
|
||||
SourceFile: fileToAdd,
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"sample.txt": {
|
||||
|
|
@ -358,11 +387,7 @@ func Test_editRun(t *testing.T) {
|
|||
reg.Register(httpmock.REST("POST", "gists/1234"),
|
||||
httpmock.StatusStringResponse(201, "{}"))
|
||||
},
|
||||
opts: &EditOptions{
|
||||
AddFilename: "from_source.txt",
|
||||
SourceFile: fileToAdd,
|
||||
},
|
||||
wantParams: map[string]interface{}{
|
||||
wantLastRequestParameters: map[string]interface{}{
|
||||
"description": "",
|
||||
"files": map[string]interface{}{
|
||||
"from_source.txt": map[string]interface{}{
|
||||
|
|
@ -374,7 +399,12 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "add file to existing gist from stdin",
|
||||
gist: &shared.Gist{
|
||||
opts: &EditOptions{
|
||||
AddFilename: "from_source.txt",
|
||||
SourceFile: "-",
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"sample.txt": {
|
||||
|
|
@ -389,12 +419,8 @@ func Test_editRun(t *testing.T) {
|
|||
reg.Register(httpmock.REST("POST", "gists/1234"),
|
||||
httpmock.StatusStringResponse(201, "{}"))
|
||||
},
|
||||
opts: &EditOptions{
|
||||
AddFilename: "from_source.txt",
|
||||
SourceFile: "-",
|
||||
},
|
||||
stdin: "data from stdin",
|
||||
wantParams: map[string]interface{}{
|
||||
wantLastRequestParameters: map[string]interface{}{
|
||||
"description": "",
|
||||
"files": map[string]interface{}{
|
||||
"from_source.txt": map[string]interface{}{
|
||||
|
|
@ -406,7 +432,11 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "remove file, file does not exist",
|
||||
gist: &shared.Gist{
|
||||
opts: &EditOptions{
|
||||
RemoveFilename: "sample2.txt",
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"sample.txt": {
|
||||
|
|
@ -417,14 +447,15 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
Owner: &shared.GistOwner{Login: "octocat"},
|
||||
},
|
||||
opts: &EditOptions{
|
||||
RemoveFilename: "sample2.txt",
|
||||
},
|
||||
wantErr: "gist has no file \"sample2.txt\"",
|
||||
},
|
||||
{
|
||||
name: "remove file from existing gist",
|
||||
gist: &shared.Gist{
|
||||
opts: &EditOptions{
|
||||
RemoveFilename: "sample2.txt",
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"sample.txt": {
|
||||
|
|
@ -444,10 +475,7 @@ func Test_editRun(t *testing.T) {
|
|||
reg.Register(httpmock.REST("POST", "gists/1234"),
|
||||
httpmock.StatusStringResponse(201, "{}"))
|
||||
},
|
||||
opts: &EditOptions{
|
||||
RemoveFilename: "sample2.txt",
|
||||
},
|
||||
wantParams: map[string]interface{}{
|
||||
wantLastRequestParameters: map[string]interface{}{
|
||||
"description": "",
|
||||
"files": map[string]interface{}{
|
||||
"sample.txt": map[string]interface{}{
|
||||
|
|
@ -460,7 +488,11 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "edit gist using file from source parameter",
|
||||
gist: &shared.Gist{
|
||||
opts: &EditOptions{
|
||||
SourceFile: fileToAdd,
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"sample.txt": {
|
||||
|
|
@ -475,10 +507,7 @@ func Test_editRun(t *testing.T) {
|
|||
reg.Register(httpmock.REST("POST", "gists/1234"),
|
||||
httpmock.StatusStringResponse(201, "{}"))
|
||||
},
|
||||
opts: &EditOptions{
|
||||
SourceFile: fileToAdd,
|
||||
},
|
||||
wantParams: map[string]interface{}{
|
||||
wantLastRequestParameters: map[string]interface{}{
|
||||
"description": "",
|
||||
"files": map[string]interface{}{
|
||||
"sample.txt": map[string]interface{}{
|
||||
|
|
@ -490,7 +519,11 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "edit gist using stdin",
|
||||
gist: &shared.Gist{
|
||||
opts: &EditOptions{
|
||||
SourceFile: "-",
|
||||
Selector: "1234",
|
||||
},
|
||||
mockGist: &shared.Gist{
|
||||
ID: "1234",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"sample.txt": {
|
||||
|
|
@ -505,11 +538,8 @@ func Test_editRun(t *testing.T) {
|
|||
reg.Register(httpmock.REST("POST", "gists/1234"),
|
||||
httpmock.StatusStringResponse(201, "{}"))
|
||||
},
|
||||
opts: &EditOptions{
|
||||
SourceFile: "-",
|
||||
},
|
||||
stdin: "data from stdin",
|
||||
wantParams: map[string]interface{}{
|
||||
wantLastRequestParameters: map[string]interface{}{
|
||||
"description": "",
|
||||
"files": map[string]interface{}{
|
||||
"sample.txt": map[string]interface{}{
|
||||
|
|
@ -519,28 +549,74 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no arguments notty",
|
||||
isTTY: false,
|
||||
opts: &EditOptions{
|
||||
Selector: "",
|
||||
},
|
||||
wantErr: "gist ID or URL required when not running interactively",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.gist == nil {
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
|
||||
if tt.opts == nil {
|
||||
tt.opts = &EditOptions{}
|
||||
}
|
||||
|
||||
if tt.opts.Selector != "" {
|
||||
// Only register the HTTP stubs for a direct gist lookup if a selector is provided.
|
||||
if tt.mockGist == nil {
|
||||
// If no gist is provided, we expect a 404.
|
||||
reg.Register(httpmock.REST("GET", fmt.Sprintf("gists/%s", tt.opts.Selector)),
|
||||
httpmock.StatusStringResponse(404, "Not Found"))
|
||||
} else {
|
||||
// If a gist is provided, we expect the gist to be fetched.
|
||||
reg.Register(httpmock.REST("GET", fmt.Sprintf("gists/%s", tt.opts.Selector)),
|
||||
httpmock.JSONResponse(tt.mockGist))
|
||||
reg.Register(httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"octocat"}}}`))
|
||||
}
|
||||
}
|
||||
|
||||
if tt.mockGistList {
|
||||
sixHours, _ := time.ParseDuration("6h")
|
||||
sixHoursAgo := time.Now().Add(-sixHours)
|
||||
reg.Register(httpmock.GraphQL(`query GistList\b`),
|
||||
httpmock.StringResponse(
|
||||
fmt.Sprintf(`{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"description": "whatever",
|
||||
"files": [{ "name": "cicada.txt" }, { "name": "unix.md" }],
|
||||
"isPublic": true,
|
||||
"name": "1234",
|
||||
"updatedAt": "%s"
|
||||
}
|
||||
],
|
||||
"pageInfo": {
|
||||
"hasNextPage": false,
|
||||
"endCursor": "somevaluedoesnotmatter"
|
||||
} } } } }`, sixHoursAgo.Format(time.RFC3339))))
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.StatusStringResponse(404, "Not Found"))
|
||||
} else {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(tt.gist))
|
||||
httpmock.JSONResponse(tt.mockGist))
|
||||
reg.Register(httpmock.GraphQL(`query UserCurrent\b`),
|
||||
httpmock.StringResponse(`{"data":{"viewer":{"login":"octocat"}}}`))
|
||||
|
||||
gistList := "cicada.txt whatever about 6 hours ago"
|
||||
pm.RegisterSelect("Select a gist",
|
||||
[]string{gistList},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, gistList)
|
||||
})
|
||||
}
|
||||
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg)
|
||||
}
|
||||
|
||||
if tt.opts == nil {
|
||||
tt.opts = &EditOptions{}
|
||||
}
|
||||
|
||||
tt.opts.Edit = func(_, _, _ string, _ *iostreams.IOStreams) (string, error) {
|
||||
return "new file content", nil
|
||||
}
|
||||
|
|
@ -550,17 +626,17 @@ func Test_editRun(t *testing.T) {
|
|||
}
|
||||
ios, stdin, stdout, stderr := iostreams.Test()
|
||||
stdin.WriteString(tt.stdin)
|
||||
ios.SetStdoutTTY(!tt.nontty)
|
||||
ios.SetStdinTTY(!tt.nontty)
|
||||
ios.SetStdoutTTY(tt.isTTY)
|
||||
ios.SetStdinTTY(tt.isTTY)
|
||||
ios.SetStderrTTY(tt.isTTY)
|
||||
|
||||
tt.opts.IO = ios
|
||||
tt.opts.Selector = "1234"
|
||||
|
||||
tt.opts.Config = func() (gh.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
if tt.prompterStubs != nil {
|
||||
tt.prompterStubs(pm)
|
||||
}
|
||||
|
|
@ -574,14 +650,21 @@ func Test_editRun(t *testing.T) {
|
|||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
if tt.wantParams != nil {
|
||||
bodyBytes, _ := io.ReadAll(reg.Requests[2].Body)
|
||||
if tt.wantLastRequestParameters != nil {
|
||||
// Currently only checking that the last request has
|
||||
// the expected request parameters.
|
||||
//
|
||||
// This might need to be changed, if a test were to be added
|
||||
// that needed to check that a request other than the last
|
||||
// has the desired parameters.
|
||||
lastRequest := reg.Requests[len(reg.Requests)-1]
|
||||
bodyBytes, _ := io.ReadAll(lastRequest.Body)
|
||||
reqBody := make(map[string]interface{})
|
||||
err = json.Unmarshal(bodyBytes, &reqBody)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding JSON: %v", err)
|
||||
}
|
||||
assert.Equal(t, tt.wantParams, reqBody)
|
||||
assert.Equal(t, tt.wantLastRequestParameters, reqBody)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", stdout.String())
|
||||
|
|
|
|||
|
|
@ -39,6 +39,19 @@ type Gist struct {
|
|||
Owner *GistOwner `json:"owner,omitempty"`
|
||||
}
|
||||
|
||||
func (g Gist) Filename() string {
|
||||
filenames := make([]string, 0, len(g.Files))
|
||||
for fn := range g.Files {
|
||||
filenames = append(filenames, fn)
|
||||
}
|
||||
sort.Strings(filenames)
|
||||
return filenames[0]
|
||||
}
|
||||
|
||||
func (g Gist) TruncDescription() string {
|
||||
return text.Truncate(100, text.RemoveExcessiveWhitespace(g.Description))
|
||||
}
|
||||
|
||||
var NotFoundErr = errors.New("not found")
|
||||
|
||||
func GetGist(client *http.Client, hostname, gistID string) (*Gist, error) {
|
||||
|
|
@ -202,47 +215,29 @@ func IsBinaryContents(contents []byte) bool {
|
|||
return isBinary
|
||||
}
|
||||
|
||||
func PromptGists(prompter prompter.Prompter, client *http.Client, host string, cs *iostreams.ColorScheme) (gistID string, err error) {
|
||||
func PromptGists(prompter prompter.Prompter, client *http.Client, host string, cs *iostreams.ColorScheme) (gist *Gist, err error) {
|
||||
gists, err := ListGists(client, host, 10, nil, false, "all")
|
||||
if err != nil {
|
||||
return "", err
|
||||
return &Gist{}, err
|
||||
}
|
||||
|
||||
if len(gists) == 0 {
|
||||
return "", nil
|
||||
return &Gist{}, nil
|
||||
}
|
||||
|
||||
var opts []string
|
||||
var gistIDs = make([]string, len(gists))
|
||||
var opts = make([]string, len(gists))
|
||||
|
||||
for i, gist := range gists {
|
||||
gistIDs[i] = gist.ID
|
||||
description := ""
|
||||
gistName := ""
|
||||
|
||||
if gist.Description != "" {
|
||||
description = gist.Description
|
||||
}
|
||||
|
||||
filenames := make([]string, 0, len(gist.Files))
|
||||
for fn := range gist.Files {
|
||||
filenames = append(filenames, fn)
|
||||
}
|
||||
sort.Strings(filenames)
|
||||
gistName = filenames[0]
|
||||
|
||||
gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt)
|
||||
// TODO: support dynamic maxWidth
|
||||
description = text.Truncate(100, text.RemoveExcessiveWhitespace(description))
|
||||
opt := fmt.Sprintf("%s %s %s", cs.Bold(gistName), description, cs.Gray(gistTime))
|
||||
opts = append(opts, opt)
|
||||
opts[i] = fmt.Sprintf("%s %s %s", cs.Bold(gist.Filename()), gist.TruncDescription(), cs.Gray(gistTime))
|
||||
}
|
||||
|
||||
result, err := prompter.Select("Select a gist", "", opts)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
return &Gist{}, err
|
||||
}
|
||||
|
||||
return gistIDs[result], nil
|
||||
return &gists[result], nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,12 +93,15 @@ func TestIsBinaryContents(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPromptGists(t *testing.T) {
|
||||
sixHours, _ := time.ParseDuration("6h")
|
||||
sixHoursAgo := time.Now().Add(-sixHours)
|
||||
sixHoursAgoFormatted := sixHoursAgo.Format(time.RFC3339Nano)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
prompterStubs func(pm *prompter.MockPrompter)
|
||||
response string
|
||||
wantOut string
|
||||
gist *Gist
|
||||
wantOut Gist
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
|
|
@ -112,21 +115,21 @@ func TestPromptGists(t *testing.T) {
|
|||
},
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "gistid1",
|
||||
"name": "1234",
|
||||
"files": [{ "name": "cool.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "gistid2",
|
||||
"name": "5678",
|
||||
"files": [{ "name": "gistfile0.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
wantOut: "gistid1",
|
||||
wantOut: Gist{ID: "1234", Files: map[string]*GistFile{"cool.txt": {Filename: "cool.txt"}}, UpdatedAt: sixHoursAgo, Public: true},
|
||||
},
|
||||
{
|
||||
name: "multiple files, select second gist",
|
||||
|
|
@ -139,26 +142,26 @@ func TestPromptGists(t *testing.T) {
|
|||
},
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "gistid1",
|
||||
"name": "1234",
|
||||
"files": [{ "name": "cool.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "gistid2",
|
||||
"name": "5678",
|
||||
"files": [{ "name": "gistfile0.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
wantOut: "gistid2",
|
||||
wantOut: Gist{ID: "5678", Files: map[string]*GistFile{"gistfile0.txt": {Filename: "gistfile0.txt"}}, UpdatedAt: sixHoursAgo, Public: true},
|
||||
},
|
||||
{
|
||||
name: "no files",
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [] } } } }`,
|
||||
wantOut: "",
|
||||
wantOut: Gist{},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -166,15 +169,12 @@ func TestPromptGists(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
|
||||
const query = `query GistList\b`
|
||||
sixHours, _ := time.ParseDuration("6h")
|
||||
sixHoursAgo := time.Now().Add(-sixHours)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(query),
|
||||
httpmock.StringResponse(fmt.Sprintf(
|
||||
tt.response,
|
||||
sixHoursAgo.Format(time.RFC3339),
|
||||
sixHoursAgoFormatted,
|
||||
)),
|
||||
)
|
||||
client := &http.Client{Transport: reg}
|
||||
|
|
@ -185,9 +185,9 @@ func TestPromptGists(t *testing.T) {
|
|||
tt.prompterStubs(mockPrompter)
|
||||
}
|
||||
|
||||
gistID, err := PromptGists(mockPrompter, client, "github.com", ios.ColorScheme())
|
||||
gist, err := PromptGists(mockPrompter, client, "github.com", ios.ColorScheme())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantOut, gistID)
|
||||
assert.Equal(t, tt.wantOut.ID, gist.ID)
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,15 +89,20 @@ func viewRun(opts *ViewOptions) error {
|
|||
|
||||
cs := opts.IO.ColorScheme()
|
||||
if gistID == "" {
|
||||
gistID, err = shared.PromptGists(opts.Prompter, client, hostname, cs)
|
||||
if !opts.IO.CanPrompt() {
|
||||
return cmdutil.FlagErrorf("gist ID or URL required when not running interactively")
|
||||
}
|
||||
|
||||
gist, err := shared.PromptGists(opts.Prompter, client, hostname, cs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gistID == "" {
|
||||
if gist.ID == "" {
|
||||
fmt.Fprintln(opts.IO.Out, "No gists found.")
|
||||
return nil
|
||||
}
|
||||
gistID = gist.ID
|
||||
}
|
||||
|
||||
if opts.Web {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdView(t *testing.T) {
|
||||
|
|
@ -94,6 +95,7 @@ func TestNewCmdView(t *testing.T) {
|
|||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
|
|
@ -114,25 +116,28 @@ func Test_viewRun(t *testing.T) {
|
|||
name string
|
||||
opts *ViewOptions
|
||||
wantOut string
|
||||
gist *shared.Gist
|
||||
wantErr bool
|
||||
mockGist *shared.Gist
|
||||
mockGistList bool
|
||||
isTTY bool
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "no such gist",
|
||||
name: "no such gist",
|
||||
isTTY: false,
|
||||
opts: &ViewOptions{
|
||||
Selector: "1234",
|
||||
ListFiles: false,
|
||||
},
|
||||
wantErr: true,
|
||||
wantErr: "not found",
|
||||
},
|
||||
{
|
||||
name: "one file",
|
||||
name: "one file",
|
||||
isTTY: true,
|
||||
opts: &ViewOptions{
|
||||
Selector: "1234",
|
||||
ListFiles: false,
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
Content: "bwhiizzzbwhuiiizzzz",
|
||||
|
|
@ -143,13 +148,14 @@ func Test_viewRun(t *testing.T) {
|
|||
wantOut: "bwhiizzzbwhuiiizzzz\n",
|
||||
},
|
||||
{
|
||||
name: "one file, no ID supplied",
|
||||
name: "one file, no ID supplied",
|
||||
isTTY: true,
|
||||
opts: &ViewOptions{
|
||||
Selector: "",
|
||||
ListFiles: false,
|
||||
},
|
||||
mockGistList: true,
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
Content: "test interactive mode",
|
||||
|
|
@ -160,13 +166,19 @@ func Test_viewRun(t *testing.T) {
|
|||
wantOut: "test interactive mode\n",
|
||||
},
|
||||
{
|
||||
name: "filename selected",
|
||||
name: "no arguments notty",
|
||||
isTTY: false,
|
||||
wantErr: "gist ID or URL required when not running interactively",
|
||||
},
|
||||
{
|
||||
name: "filename selected",
|
||||
isTTY: true,
|
||||
opts: &ViewOptions{
|
||||
Selector: "1234",
|
||||
Filename: "cicada.txt",
|
||||
ListFiles: false,
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
Content: "bwhiizzzbwhuiiizzzz",
|
||||
|
|
@ -181,14 +193,15 @@ func Test_viewRun(t *testing.T) {
|
|||
wantOut: "bwhiizzzbwhuiiizzzz\n",
|
||||
},
|
||||
{
|
||||
name: "filename selected, raw",
|
||||
name: "filename selected, raw",
|
||||
isTTY: true,
|
||||
opts: &ViewOptions{
|
||||
Selector: "1234",
|
||||
Filename: "cicada.txt",
|
||||
Raw: true,
|
||||
ListFiles: false,
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
Content: "bwhiizzzbwhuiiizzzz",
|
||||
|
|
@ -203,12 +216,13 @@ func Test_viewRun(t *testing.T) {
|
|||
wantOut: "bwhiizzzbwhuiiizzzz\n",
|
||||
},
|
||||
{
|
||||
name: "multiple files, no description",
|
||||
name: "multiple files, no description",
|
||||
isTTY: true,
|
||||
opts: &ViewOptions{
|
||||
Selector: "1234",
|
||||
ListFiles: false,
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
Content: "bwhiizzzbwhuiiizzzz",
|
||||
|
|
@ -220,15 +234,16 @@ func Test_viewRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
wantOut: "cicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n\n # foo \n\n",
|
||||
wantOut: "cicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n\n # foo \n\n",
|
||||
},
|
||||
{
|
||||
name: "multiple files, trailing newlines",
|
||||
name: "multiple files, trailing newlines",
|
||||
isTTY: true,
|
||||
opts: &ViewOptions{
|
||||
Selector: "1234",
|
||||
ListFiles: false,
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
Content: "bwhiizzzbwhuiiizzzz\n",
|
||||
|
|
@ -243,12 +258,13 @@ func Test_viewRun(t *testing.T) {
|
|||
wantOut: "cicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.txt\n\nbar\n",
|
||||
},
|
||||
{
|
||||
name: "multiple files, description",
|
||||
name: "multiple files, description",
|
||||
isTTY: true,
|
||||
opts: &ViewOptions{
|
||||
Selector: "1234",
|
||||
ListFiles: false,
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
Description: "some files",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
|
|
@ -261,16 +277,17 @@ func Test_viewRun(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
wantOut: "some files\n\ncicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n\n \n • foo \n\n",
|
||||
wantOut: "some files\n\ncicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n\n \n • foo \n\n",
|
||||
},
|
||||
{
|
||||
name: "multiple files, raw",
|
||||
name: "multiple files, raw",
|
||||
isTTY: true,
|
||||
opts: &ViewOptions{
|
||||
Selector: "1234",
|
||||
Raw: true,
|
||||
ListFiles: false,
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
Description: "some files",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
|
|
@ -286,13 +303,14 @@ func Test_viewRun(t *testing.T) {
|
|||
wantOut: "some files\n\ncicada.txt\n\nbwhiizzzbwhuiiizzzz\n\nfoo.md\n\n- foo\n",
|
||||
},
|
||||
{
|
||||
name: "one file, list files",
|
||||
name: "one file, list files",
|
||||
isTTY: true,
|
||||
opts: &ViewOptions{
|
||||
Selector: "1234",
|
||||
Raw: false,
|
||||
ListFiles: true,
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
Description: "some files",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
|
|
@ -304,13 +322,14 @@ func Test_viewRun(t *testing.T) {
|
|||
wantOut: "cicada.txt\n",
|
||||
},
|
||||
{
|
||||
name: "multiple file, list files",
|
||||
name: "multiple file, list files",
|
||||
isTTY: true,
|
||||
opts: &ViewOptions{
|
||||
Selector: "1234",
|
||||
Raw: false,
|
||||
ListFiles: true,
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
mockGist: &shared.Gist{
|
||||
Description: "some files",
|
||||
Files: map[string]*shared.GistFile{
|
||||
"cicada.txt": {
|
||||
|
|
@ -329,12 +348,12 @@ func Test_viewRun(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
if tt.gist == nil {
|
||||
if tt.mockGist == nil {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.StatusStringResponse(404, "Not Found"))
|
||||
} else {
|
||||
reg.Register(httpmock.REST("GET", "gists/1234"),
|
||||
httpmock.JSONResponse(tt.gist))
|
||||
httpmock.JSONResponse(tt.mockGist))
|
||||
}
|
||||
|
||||
if tt.opts == nil {
|
||||
|
|
@ -376,16 +395,20 @@ func Test_viewRun(t *testing.T) {
|
|||
}
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStdoutTTY(tt.isTTY)
|
||||
ios.SetStdinTTY(tt.isTTY)
|
||||
ios.SetStderrTTY(tt.isTTY)
|
||||
|
||||
tt.opts.IO = ios
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := viewRun(tt.opts)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.wantErr != "" {
|
||||
require.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wantOut, stdout.String())
|
||||
reg.Verify(t)
|
||||
|
|
|
|||
|
|
@ -263,17 +263,17 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
return cmd
|
||||
}
|
||||
|
||||
func createRun(opts *CreateOptions) (err error) {
|
||||
func createRun(opts *CreateOptions) error {
|
||||
ctx, err := NewCreateContext(opts)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
client := ctx.Client
|
||||
|
||||
state, err := NewIssueState(*ctx, *opts)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
var openURL string
|
||||
|
|
@ -288,15 +288,15 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
}
|
||||
err = handlePush(*opts, *ctx)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
openURL, err = generateCompareURL(*ctx, *state)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
if !shared.ValidURL(openURL) {
|
||||
err = fmt.Errorf("cannot open in browser: maximum URL length exceeded")
|
||||
return
|
||||
return err
|
||||
}
|
||||
return previewPR(*opts, openURL)
|
||||
}
|
||||
|
|
@ -344,7 +344,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
if !opts.EditorMode && (opts.FillVerbose || opts.Autofill || opts.FillFirst || (opts.TitleProvided && opts.BodyProvided)) {
|
||||
err = handlePush(*opts, *ctx)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
return submitPR(*opts, *ctx, *state)
|
||||
}
|
||||
|
|
@ -368,7 +368,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
var template shared.Template
|
||||
template, err = tpl.Select(opts.Template)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
if state.Title == "" {
|
||||
state.Title = template.Title()
|
||||
|
|
@ -378,18 +378,18 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
|
||||
state.Title, state.Body, err = opts.TitledEditSurvey(state.Title, state.Body)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
if state.Title == "" {
|
||||
err = fmt.Errorf("title can't be blank")
|
||||
return
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
||||
if !opts.TitleProvided {
|
||||
err = shared.TitleSurvey(opts.Prompter, opts.IO, state)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -403,12 +403,12 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
if opts.Template != "" {
|
||||
template, err = tpl.Select(opts.Template)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
template, err = tpl.Choose()
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -419,13 +419,13 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
|
||||
err = shared.BodySurvey(opts.Prompter, state, templateContent)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
openURL, err = generateCompareURL(*ctx, *state)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
allowPreview := !state.HasMetadata() && shared.ValidURL(openURL) && !opts.DryRun
|
||||
|
|
@ -444,12 +444,12 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
}
|
||||
err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.BaseRepo, fetcher, state)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
action, err = shared.ConfirmPRSubmission(opts.Prompter, !state.HasMetadata() && !opts.DryRun, false, state.Draft)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -457,12 +457,12 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
if action == shared.CancelAction {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
|
||||
err = cmdutil.CancelError
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
err = handlePush(*opts, *ctx)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if action == shared.PreviewAction {
|
||||
|
|
@ -479,7 +479,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
}
|
||||
|
||||
err = errors.New("expected to cancel, preview, or submit")
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
var regexPattern = regexp.MustCompile(`(?m)^`)
|
||||
|
|
@ -680,7 +680,10 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) {
|
|||
var headRepo ghrepo.Interface
|
||||
var headRemote *ghContext.Remote
|
||||
|
||||
headBranchConfig := gitClient.ReadBranchConfig(context.Background(), headBranch)
|
||||
headBranchConfig, err := gitClient.ReadBranchConfig(context.Background(), headBranch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isPushEnabled {
|
||||
// determine whether the head branch is already pushed to a remote
|
||||
if trackingRef, found := tryDetermineTrackingRef(gitClient, remotes, headBranch, headBranchConfig); found {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
ctx "context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -497,7 +496,7 @@ func Test_createRun(t *testing.T) {
|
|||
`MaintainerCanModify: false`,
|
||||
`Body:`,
|
||||
``,
|
||||
` my body `,
|
||||
` my body `,
|
||||
``,
|
||||
``,
|
||||
},
|
||||
|
|
@ -569,7 +568,7 @@ func Test_createRun(t *testing.T) {
|
|||
`MaintainerCanModify: false`,
|
||||
`Body:`,
|
||||
``,
|
||||
` BODY `,
|
||||
` BODY `,
|
||||
``,
|
||||
``,
|
||||
},
|
||||
|
|
@ -1626,6 +1625,7 @@ func Test_tryDetermineTrackingRef(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
cmdStubs func(*run.CommandStubber)
|
||||
headBranchConfig git.BranchConfig
|
||||
remotes context.Remotes
|
||||
expectedTrackingRef trackingRef
|
||||
expectedFound bool
|
||||
|
|
@ -1633,18 +1633,18 @@ func Test_tryDetermineTrackingRef(t *testing.T) {
|
|||
{
|
||||
name: "empty",
|
||||
cmdStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
|
||||
cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD")
|
||||
},
|
||||
headBranchConfig: git.BranchConfig{},
|
||||
expectedTrackingRef: trackingRef{},
|
||||
expectedFound: false,
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
cmdStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
|
||||
cs.Register("git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature", 0, "abc HEAD\nbca refs/remotes/upstream/feature")
|
||||
},
|
||||
headBranchConfig: git.BranchConfig{},
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
|
|
@ -1661,13 +1661,13 @@ func Test_tryDetermineTrackingRef(t *testing.T) {
|
|||
{
|
||||
name: "match",
|
||||
cmdStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "")
|
||||
cs.Register(`git show-ref --verify -- HEAD refs/remotes/upstream/feature refs/remotes/origin/feature$`, 0, heredoc.Doc(`
|
||||
deadbeef HEAD
|
||||
deadb00f refs/remotes/upstream/feature
|
||||
deadbeef refs/remotes/origin/feature
|
||||
`))
|
||||
},
|
||||
headBranchConfig: git.BranchConfig{},
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
|
|
@ -1687,15 +1687,15 @@ func Test_tryDetermineTrackingRef(t *testing.T) {
|
|||
{
|
||||
name: "respect tracking config",
|
||||
cmdStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, heredoc.Doc(`
|
||||
branch.feature.remote origin
|
||||
branch.feature.merge refs/heads/great-feat
|
||||
`))
|
||||
cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/great-feat refs/remotes/origin/feature$`, 0, heredoc.Doc(`
|
||||
deadbeef HEAD
|
||||
deadb00f refs/remotes/origin/feature
|
||||
`))
|
||||
},
|
||||
headBranchConfig: git.BranchConfig{
|
||||
RemoteName: "origin",
|
||||
MergeRef: "refs/heads/great-feat",
|
||||
},
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
|
|
@ -1717,8 +1717,8 @@ func Test_tryDetermineTrackingRef(t *testing.T) {
|
|||
GhPath: "some/path/gh",
|
||||
GitPath: "some/path/git",
|
||||
}
|
||||
headBranchConfig := gitClient.ReadBranchConfig(ctx.Background(), "feature")
|
||||
ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", headBranchConfig)
|
||||
|
||||
ref, found := tryDetermineTrackingRef(gitClient, tt.remotes, "feature", tt.headBranchConfig)
|
||||
|
||||
assert.Equal(t, tt.expectedTrackingRef, ref)
|
||||
assert.Equal(t, tt.expectedFound, found)
|
||||
|
|
|
|||
|
|
@ -384,13 +384,14 @@ func (m *mergeContext) deleteLocalBranch() error {
|
|||
|
||||
if m.merged {
|
||||
if m.opts.IO.CanPrompt() && !m.opts.IsDeleteBranchIndicated {
|
||||
confirmed, err := m.opts.Prompter.Confirm(fmt.Sprintf("Pull request %s#%d was already merged. Delete the branch locally?", ghrepo.FullName(m.baseRepo), m.pr.Number), false)
|
||||
message := fmt.Sprintf("Pull request %s#%d was already merged. Delete the branch locally?", ghrepo.FullName(m.baseRepo), m.pr.Number)
|
||||
confirmed, err := m.opts.Prompter.Confirm(message, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
m.deleteBranch = confirmed
|
||||
} else {
|
||||
_ = m.warnf(fmt.Sprintf("%s Pull request %s#%d was already merged\n", m.cs.WarningIcon(), ghrepo.FullName(m.baseRepo), m.pr.Number))
|
||||
_ = m.warnf("%s Pull request %s#%d was already merged\n", m.cs.WarningIcon(), ghrepo.FullName(m.baseRepo), m.pr.Number)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -431,7 +432,7 @@ func (m *mergeContext) deleteLocalBranch() error {
|
|||
}
|
||||
|
||||
if err := m.opts.GitClient.Pull(ctx, baseRemote.Name, targetBranch); err != nil {
|
||||
_ = m.warnf(fmt.Sprintf("%s warning: not possible to fast-forward to: %q\n", m.cs.WarningIcon(), targetBranch))
|
||||
_ = m.warnf("%s warning: not possible to fast-forward to: %q\n", m.cs.WarningIcon(), targetBranch)
|
||||
}
|
||||
|
||||
switchedToBranch = targetBranch
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ func TestPRReview_interactive(t *testing.T) {
|
|||
assert.Equal(t, heredoc.Doc(`
|
||||
Got:
|
||||
|
||||
cool story
|
||||
cool story
|
||||
|
||||
`), output.String())
|
||||
assert.Equal(t, "✓ Approved pull request OWNER/REPO#123\n", output.Stderr())
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ type finder struct {
|
|||
branchFn func() (string, error)
|
||||
remotesFn func() (remotes.Remotes, error)
|
||||
httpClient func() (*http.Client, error)
|
||||
branchConfig func(string) git.BranchConfig
|
||||
branchConfig func(string) (git.BranchConfig, error)
|
||||
progress progressIndicator
|
||||
|
||||
repo ghrepo.Interface
|
||||
|
|
@ -58,7 +58,7 @@ func NewFinder(factory *cmdutil.Factory) PRFinder {
|
|||
remotesFn: factory.Remotes,
|
||||
httpClient: factory.HttpClient,
|
||||
progress: factory.IOStreams,
|
||||
branchConfig: func(s string) git.BranchConfig {
|
||||
branchConfig: func(s string) (git.BranchConfig, error) {
|
||||
return factory.GitClient.ReadBranchConfig(context.Background(), s)
|
||||
},
|
||||
}
|
||||
|
|
@ -238,7 +238,10 @@ func (f *finder) parseCurrentBranch() (string, int, error) {
|
|||
return "", 0, err
|
||||
}
|
||||
|
||||
branchConfig := f.branchConfig(prHeadRef)
|
||||
branchConfig, err := f.branchConfig(prHeadRef)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
// the branch is configured to merge a special PR head ref
|
||||
if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
|
||||
|
|
|
|||
|
|
@ -10,19 +10,21 @@ import (
|
|||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type args struct {
|
||||
baseRepoFn func() (ghrepo.Interface, error)
|
||||
branchFn func() (string, error)
|
||||
branchConfig func(string) (git.BranchConfig, error)
|
||||
remotesFn func() (context.Remotes, error)
|
||||
selector string
|
||||
fields []string
|
||||
baseBranch string
|
||||
}
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
type args struct {
|
||||
baseRepoFn func() (ghrepo.Interface, error)
|
||||
branchFn func() (string, error)
|
||||
branchConfig func(string) git.BranchConfig
|
||||
remotesFn func() (context.Remotes, error)
|
||||
selector string
|
||||
fields []string
|
||||
baseBranch string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
|
|
@ -231,9 +233,7 @@ func TestFind(t *testing.T) {
|
|||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
return
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -265,9 +265,7 @@ func TestFind(t *testing.T) {
|
|||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
return
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -315,11 +313,10 @@ func TestFind(t *testing.T) {
|
|||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
c.MergeRef = "refs/heads/blue-upstream-berries"
|
||||
c.RemoteName = "origin"
|
||||
return
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{
|
||||
MergeRef: "refs/heads/blue-upstream-berries",
|
||||
RemoteName: "origin",
|
||||
}, nil),
|
||||
remotesFn: func() (context.Remotes, error) {
|
||||
return context.Remotes{{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
|
|
@ -357,11 +354,12 @@ func TestFind(t *testing.T) {
|
|||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
branchConfig: func(branch string) (git.BranchConfig, error) {
|
||||
u, _ := url.Parse("https://github.com/UPSTREAMOWNER/REPO")
|
||||
c.MergeRef = "refs/heads/blue-upstream-berries"
|
||||
c.RemoteURL = u
|
||||
return
|
||||
return stubBranchConfig(git.BranchConfig{
|
||||
MergeRef: "refs/heads/blue-upstream-berries",
|
||||
RemoteURL: u,
|
||||
}, nil)(branch)
|
||||
},
|
||||
remotesFn: nil,
|
||||
},
|
||||
|
|
@ -395,10 +393,9 @@ func TestFind(t *testing.T) {
|
|||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
c.RemoteName = "origin"
|
||||
return
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{
|
||||
RemoteName: "origin",
|
||||
}, nil),
|
||||
remotesFn: func() (context.Remotes, error) {
|
||||
return context.Remotes{{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
|
|
@ -436,10 +433,9 @@ func TestFind(t *testing.T) {
|
|||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
c.MergeRef = "refs/pull/13/head"
|
||||
return
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{
|
||||
MergeRef: "refs/pull/13/head",
|
||||
}, nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -462,10 +458,9 @@ func TestFind(t *testing.T) {
|
|||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
branchConfig: func(branch string) (c git.BranchConfig) {
|
||||
c.MergeRef = "refs/pull/13/head"
|
||||
return
|
||||
},
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{
|
||||
MergeRef: "refs/pull/13/head",
|
||||
}, nil),
|
||||
},
|
||||
httpStub: func(r *httpmock.Registry) {
|
||||
r.Register(
|
||||
|
|
@ -561,3 +556,49 @@ func TestFind(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseCurrentBranch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantSelector string
|
||||
wantPR int
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
name: "failed branch config",
|
||||
args: args{
|
||||
branchConfig: stubBranchConfig(git.BranchConfig{}, errors.New("branchConfigErr")),
|
||||
branchFn: func() (string, error) {
|
||||
return "blueberries", nil
|
||||
},
|
||||
},
|
||||
wantSelector: "",
|
||||
wantPR: 0,
|
||||
wantError: errors.New("branchConfigErr"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := finder{
|
||||
httpClient: func() (*http.Client, error) {
|
||||
return &http.Client{}, nil
|
||||
},
|
||||
baseRepoFn: tt.args.baseRepoFn,
|
||||
branchFn: tt.args.branchFn,
|
||||
branchConfig: tt.args.branchConfig,
|
||||
remotesFn: tt.args.remotesFn,
|
||||
}
|
||||
selector, pr, err := f.parseCurrentBranch()
|
||||
assert.Equal(t, tt.wantSelector, selector)
|
||||
assert.Equal(t, tt.wantPR, pr)
|
||||
assert.Equal(t, tt.wantError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stubBranchConfig(branchConfig git.BranchConfig, err error) func(string) (git.BranchConfig, error) {
|
||||
return func(branch string) (git.BranchConfig, error) {
|
||||
return branchConfig, err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
|
|||
}
|
||||
|
||||
func statusRun(opts *StatusOptions) error {
|
||||
ctx := context.Background()
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -93,7 +94,11 @@ func statusRun(opts *StatusOptions) error {
|
|||
}
|
||||
|
||||
remotes, _ := opts.Remotes()
|
||||
currentPRNumber, currentPRHeadRef, err = prSelectorForCurrentBranch(opts.GitClient, baseRepo, currentBranch, remotes)
|
||||
branchConfig, err := opts.GitClient.ReadBranchConfig(ctx, currentBranch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentPRNumber, currentPRHeadRef, err = prSelectorForCurrentBranch(branchConfig, baseRepo, currentBranch, remotes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not query for pull request for current branch: %w", err)
|
||||
}
|
||||
|
|
@ -184,31 +189,42 @@ func statusRun(opts *StatusOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface, prHeadRef string, rem ghContext.Remotes) (prNumber int, selector string, err error) {
|
||||
selector = prHeadRef
|
||||
branchConfig := gitClient.ReadBranchConfig(context.Background(), prHeadRef)
|
||||
|
||||
func prSelectorForCurrentBranch(branchConfig git.BranchConfig, baseRepo ghrepo.Interface, prHeadRef string, rem ghContext.Remotes) (int, string, error) {
|
||||
// the branch is configured to merge a special PR head ref
|
||||
prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`)
|
||||
if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
|
||||
prNumber, _ = strconv.Atoi(m[1])
|
||||
return
|
||||
prNumber, err := strconv.Atoi(m[1])
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
return prNumber, prHeadRef, nil
|
||||
}
|
||||
|
||||
var branchOwner string
|
||||
if branchConfig.RemoteURL != nil {
|
||||
// the branch merges from a remote specified by URL
|
||||
if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil {
|
||||
branchOwner = r.RepoOwner()
|
||||
r, err := ghrepo.FromURL(branchConfig.RemoteURL)
|
||||
if err != nil {
|
||||
// TODO: We aren't returning the error because we discovered that it was shadowed
|
||||
// before refactoring to its current return pattern. Thus, we aren't confident
|
||||
// that returning the error won't break existing behavior.
|
||||
return 0, prHeadRef, nil
|
||||
}
|
||||
branchOwner = r.RepoOwner()
|
||||
} else if branchConfig.RemoteName != "" {
|
||||
// the branch merges from a remote specified by name
|
||||
if r, err := rem.FindByName(branchConfig.RemoteName); err == nil {
|
||||
branchOwner = r.RepoOwner()
|
||||
r, err := rem.FindByName(branchConfig.RemoteName)
|
||||
if err != nil {
|
||||
// TODO: We aren't returning the error because we discovered that it was shadowed
|
||||
// before refactoring to its current return pattern. Thus, we aren't confident
|
||||
// that returning the error won't break existing behavior.
|
||||
return 0, prHeadRef, nil
|
||||
}
|
||||
branchOwner = r.RepoOwner()
|
||||
}
|
||||
|
||||
if branchOwner != "" {
|
||||
selector := prHeadRef
|
||||
if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") {
|
||||
selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/")
|
||||
}
|
||||
|
|
@ -216,9 +232,10 @@ func prSelectorForCurrentBranch(gitClient *git.Client, baseRepo ghrepo.Interface
|
|||
if !strings.EqualFold(branchOwner, baseRepo.RepoOwner()) {
|
||||
selector = fmt.Sprintf("%s:%s", branchOwner, selector)
|
||||
}
|
||||
return 0, selector, nil
|
||||
}
|
||||
|
||||
return
|
||||
return 0, prHeadRef, nil
|
||||
}
|
||||
|
||||
func totalApprovals(pr *api.PullRequest) int {
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import (
|
|||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/test"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
|
|
@ -95,6 +96,11 @@ func TestPRStatus(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatus.json"))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -120,6 +126,11 @@ func TestPRStatus_reviewsAndChecks(t *testing.T) {
|
|||
// status,conclusion matches the old StatusContextRollup query
|
||||
http.Register(httpmock.GraphQL(`status,conclusion`), httpmock.FileResponse("./fixtures/prStatusChecks.json"))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -145,6 +156,11 @@ func TestPRStatus_reviewsAndChecksWithStatesByCount(t *testing.T) {
|
|||
// checkRunCount,checkRunCountsByState matches the new StatusContextRollup query
|
||||
http.Register(httpmock.GraphQL(`checkRunCount,checkRunCountsByState`), httpmock.FileResponse("./fixtures/prStatusChecksWithStatesByCount.json"))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommandWithDetector(http, "blueberries", true, "", &fd.EnabledDetectorMock{})
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -169,6 +185,11 @@ func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranch.json"))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -197,6 +218,11 @@ func TestPRStatus_currentBranch_defaultBranch(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranch.json"))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -231,6 +257,11 @@ func TestPRStatus_currentBranch_Closed(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchClosed.json"))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -248,6 +279,11 @@ func TestPRStatus_currentBranch_Closed_defaultBranch(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchClosedOnDefaultBranch.json"))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -265,6 +301,11 @@ func TestPRStatus_currentBranch_Merged(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchMerged.json"))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -282,6 +323,11 @@ func TestPRStatus_currentBranch_Merged_defaultBranch(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusCurrentBranchMergedOnDefaultBranch.json"))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -299,6 +345,11 @@ func TestPRStatus_blankSlate(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.StringResponse(`{"data": {}}`))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -352,6 +403,11 @@ func TestPRStatus_detachedHead(t *testing.T) {
|
|||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.StringResponse(`{"data": {}}`))
|
||||
|
||||
// stub successful git command
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, "")
|
||||
|
||||
output, err := runCommand(http, "", true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
|
|
@ -375,31 +431,219 @@ Requesting a code review from you
|
|||
}
|
||||
}
|
||||
|
||||
func Test_prSelectorForCurrentBranch(t *testing.T) {
|
||||
func TestPRStatus_error_ReadBranchConfig(t *testing.T) {
|
||||
rs, cleanup := run.Stub()
|
||||
defer cleanup(t)
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 1, "")
|
||||
|
||||
rs.Register(`git config --get-regexp \^branch\\.`, 0, heredoc.Doc(`
|
||||
branch.Frederick888/main.remote git@github.com:Frederick888/playground.git
|
||||
branch.Frederick888/main.merge refs/heads/main
|
||||
`))
|
||||
_, err := runCommand(initFakeHTTP(), "blueberries", true, "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
repo := ghrepo.NewWithHost("octocat", "playground", "github.com")
|
||||
rem := context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: repo,
|
||||
func Test_prSelectorForCurrentBranch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
branchConfig git.BranchConfig
|
||||
baseRepo ghrepo.Interface
|
||||
prHeadRef string
|
||||
remotes context.Remotes
|
||||
wantPrNumber int
|
||||
wantSelector string
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
name: "Empty branch config",
|
||||
branchConfig: git.BranchConfig{},
|
||||
prHeadRef: "monalisa/main",
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "The branch is configured to merge a special PR head ref",
|
||||
branchConfig: git.BranchConfig{
|
||||
MergeRef: "refs/pull/42/head",
|
||||
},
|
||||
prHeadRef: "monalisa/main",
|
||||
wantPrNumber: 42,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch merges from a remote specified by URL",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteURL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "monalisa/playground.git",
|
||||
},
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch merges from a remote specified by name",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: "upstream",
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch is a fork and merges from a remote specified by URL",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteURL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "forkName/playground.git",
|
||||
},
|
||||
MergeRef: "refs/heads/main",
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "forkName:main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch is a fork and merges from a remote specified by name",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: "origin",
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "forkName:monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch specifies a mergeRef and merges from a remote specified by name",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: "upstream",
|
||||
MergeRef: "refs/heads/main",
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Branch is a fork, specifies a mergeRef, and merges from a remote specified by name",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: "origin",
|
||||
MergeRef: "refs/heads/main",
|
||||
},
|
||||
baseRepo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "forkName:main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Remote URL errors",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteURL: &url.URL{
|
||||
Scheme: "ssh",
|
||||
User: url.User("git"),
|
||||
Host: "github.com",
|
||||
Path: "/\\invalid?Path/",
|
||||
},
|
||||
},
|
||||
prHeadRef: "monalisa/main",
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
{
|
||||
name: "Remote Name errors",
|
||||
branchConfig: git.BranchConfig{
|
||||
RemoteName: "nonexistentRemote",
|
||||
},
|
||||
prHeadRef: "monalisa/main",
|
||||
remotes: context.Remotes{
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.NewWithHost("forkName", "playground", "github.com"),
|
||||
},
|
||||
&context.Remote{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.NewWithHost("monalisa", "playground", "github.com"),
|
||||
},
|
||||
},
|
||||
wantPrNumber: 0,
|
||||
wantSelector: "monalisa/main",
|
||||
wantError: nil,
|
||||
},
|
||||
}
|
||||
gitClient := &git.Client{GitPath: "some/path/git"}
|
||||
prNum, headRef, err := prSelectorForCurrentBranch(gitClient, repo, "Frederick888/main", rem)
|
||||
if err != nil {
|
||||
t.Fatalf("prSelectorForCurrentBranch error: %v", err)
|
||||
}
|
||||
if prNum != 0 {
|
||||
t.Errorf("expected prNum to be 0, got %q", prNum)
|
||||
}
|
||||
if headRef != "Frederick888:main" {
|
||||
t.Errorf("expected headRef to be \"Frederick888:main\", got %q", headRef)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
prNum, headRef, err := prSelectorForCurrentBranch(tt.branchConfig, tt.baseRepo, tt.prHeadRef, tt.remotes)
|
||||
assert.Equal(t, tt.wantPrNumber, prNum)
|
||||
assert.Equal(t, tt.wantSelector, headRef)
|
||||
assert.Equal(t, tt.wantError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,8 +145,8 @@ func Test_viewRun(t *testing.T) {
|
|||
v1.2.3
|
||||
MonaLisa released this about 1 day ago
|
||||
|
||||
|
||||
• Fixed bugs
|
||||
|
||||
• Fixed bugs
|
||||
|
||||
|
||||
Assets
|
||||
|
|
@ -169,8 +169,8 @@ func Test_viewRun(t *testing.T) {
|
|||
v1.2.3
|
||||
MonaLisa released this about 1 day ago
|
||||
|
||||
|
||||
• Fixed bugs
|
||||
|
||||
• Fixed bugs
|
||||
|
||||
|
||||
Assets
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ func explainer() string {
|
|||
- viewing and creating issues
|
||||
- viewing and creating releases
|
||||
- working with GitHub Actions
|
||||
- adding repository and environment secrets`)
|
||||
|
||||
### NOTE: gh does not use the default repository for managing repository and environment secrets.`)
|
||||
}
|
||||
|
||||
type iprompter interface {
|
||||
|
|
|
|||
|
|
@ -382,7 +382,7 @@ func TestDefaultRun(t *testing.T) {
|
|||
}
|
||||
}
|
||||
},
|
||||
wantStdout: "This command sets the default remote repository to use when querying the\nGitHub API for the locally cloned repository.\n\ngh uses the default repository for things like:\n\n - viewing and creating pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with GitHub Actions\n - adding repository and environment secrets\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n",
|
||||
wantStdout: "This command sets the default remote repository to use when querying the\nGitHub API for the locally cloned repository.\n\ngh uses the default repository for things like:\n\n - viewing and creating pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with GitHub Actions\n\n### NOTE: gh does not use the default repository for managing repository and environment secrets.\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n",
|
||||
},
|
||||
{
|
||||
name: "interactive mode only one known host",
|
||||
|
|
@ -456,7 +456,7 @@ func TestDefaultRun(t *testing.T) {
|
|||
}
|
||||
}
|
||||
},
|
||||
wantStdout: "This command sets the default remote repository to use when querying the\nGitHub API for the locally cloned repository.\n\ngh uses the default repository for things like:\n\n - viewing and creating pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with GitHub Actions\n - adding repository and environment secrets\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n",
|
||||
wantStdout: "This command sets the default remote repository to use when querying the\nGitHub API for the locally cloned repository.\n\ngh uses the default repository for things like:\n\n - viewing and creating pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with GitHub Actions\n\n### NOTE: gh does not use the default repository for managing repository and environment secrets.\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ func viewRun(opts *ViewOptions) error {
|
|||
fmt.Fprintf(stdout, "description:\t%s\n", repo.Description)
|
||||
if readme != nil {
|
||||
fmt.Fprintln(stdout, "--")
|
||||
fmt.Fprintf(stdout, readme.Content)
|
||||
fmt.Fprint(stdout, readme.Content)
|
||||
fmt.Fprintln(stdout)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ func Test_ViewRun(t *testing.T) {
|
|||
social distancing
|
||||
|
||||
|
||||
# truly cool readme check it out
|
||||
# truly cool readme check it out
|
||||
|
||||
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ func Test_ViewRun(t *testing.T) {
|
|||
social distancing
|
||||
|
||||
|
||||
# truly cool readme check it out
|
||||
# truly cool readme check it out
|
||||
|
||||
|
||||
|
||||
|
|
@ -297,7 +297,7 @@ func Test_ViewRun(t *testing.T) {
|
|||
social distancing
|
||||
|
||||
|
||||
# truly cool readme check it out
|
||||
# truly cool readme check it out
|
||||
|
||||
|
||||
|
||||
|
|
@ -312,7 +312,7 @@ func Test_ViewRun(t *testing.T) {
|
|||
social distancing
|
||||
|
||||
|
||||
# truly cool readme check it out
|
||||
# truly cool readme check it out
|
||||
|
||||
|
||||
|
||||
|
|
@ -650,7 +650,7 @@ func Test_ViewRun_HandlesSpecialCharacters(t *testing.T) {
|
|||
Some basic special characters " & / < > '
|
||||
|
||||
|
||||
# < is always > than & ' and "
|
||||
# < is always > than & ' and "
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -52,20 +52,25 @@ func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ex
|
|||
},
|
||||
// PostRun handles communicating extension release information if found
|
||||
PostRun: func(c *cobra.Command, args []string) {
|
||||
releaseInfo := <-updateMessageChan
|
||||
if releaseInfo != nil {
|
||||
stderr := io.ErrOut
|
||||
fmt.Fprintf(stderr, "\n\n%s %s → %s\n",
|
||||
cs.Yellowf("A new release of %s is available:", ext.Name()),
|
||||
cs.Cyan(strings.TrimPrefix(ext.CurrentVersion(), "v")),
|
||||
cs.Cyan(strings.TrimPrefix(releaseInfo.Version, "v")))
|
||||
if ext.IsPinned() {
|
||||
fmt.Fprintf(stderr, "To upgrade, run: gh extension upgrade %s --force\n", ext.Name())
|
||||
} else {
|
||||
fmt.Fprintf(stderr, "To upgrade, run: gh extension upgrade %s\n", ext.Name())
|
||||
select {
|
||||
case releaseInfo := <-updateMessageChan:
|
||||
if releaseInfo != nil {
|
||||
stderr := io.ErrOut
|
||||
fmt.Fprintf(stderr, "\n\n%s %s → %s\n",
|
||||
cs.Yellowf("A new release of %s is available:", ext.Name()),
|
||||
cs.Cyan(strings.TrimPrefix(ext.CurrentVersion(), "v")),
|
||||
cs.Cyan(strings.TrimPrefix(releaseInfo.Version, "v")))
|
||||
if ext.IsPinned() {
|
||||
fmt.Fprintf(stderr, "To upgrade, run: gh extension upgrade %s --force\n", ext.Name())
|
||||
} else {
|
||||
fmt.Fprintf(stderr, "To upgrade, run: gh extension upgrade %s\n", ext.Name())
|
||||
}
|
||||
fmt.Fprintf(stderr, "%s\n\n",
|
||||
cs.Yellow(releaseInfo.URL))
|
||||
}
|
||||
fmt.Fprintf(stderr, "%s\n\n",
|
||||
cs.Yellow(releaseInfo.URL))
|
||||
default:
|
||||
// Do not make the user wait for extension update check if incomplete by this time.
|
||||
// This is being handled in non-blocking default as there is no context to cancel like in gh update checks.
|
||||
}
|
||||
},
|
||||
GroupID: "extension",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package root_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/update"
|
||||
|
|
@ -121,6 +123,10 @@ func TestNewCmdExtension_Updates(t *testing.T) {
|
|||
em := &extensions.ExtensionManagerMock{
|
||||
DispatchFunc: func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) {
|
||||
// Assume extension executed / dispatched without problems as test is focused on upgrade checking.
|
||||
// Sleep for 100 milliseconds to allow update checking logic to complete. This would be better
|
||||
// served by making the behaviour controllable by channels, but it's a larger change than desired
|
||||
// just to improve the test.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return true, nil
|
||||
},
|
||||
}
|
||||
|
|
@ -169,3 +175,62 @@ func TestNewCmdExtension_Updates(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCmdExtension_UpdateCheckIsNonblocking(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
em := &extensions.ExtensionManagerMock{
|
||||
DispatchFunc: func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) {
|
||||
// Assume extension executed / dispatched without problems as test is focused on upgrade checking.
|
||||
return true, nil
|
||||
},
|
||||
}
|
||||
|
||||
ext := &extensions.ExtensionMock{
|
||||
CurrentVersionFunc: func() string {
|
||||
return "1.0.0"
|
||||
},
|
||||
IsPinnedFunc: func() bool {
|
||||
return false
|
||||
},
|
||||
LatestVersionFunc: func() string {
|
||||
return "2.0.0"
|
||||
},
|
||||
NameFunc: func() string {
|
||||
return "major-update"
|
||||
},
|
||||
UpdateAvailableFunc: func() bool {
|
||||
return true
|
||||
},
|
||||
URLFunc: func() string {
|
||||
return "https//github.com/dne/major-update"
|
||||
},
|
||||
}
|
||||
|
||||
// When the extension command is executed, the checkFunc will run in the background longer than the extension dispatch.
|
||||
// If the update check is non-blocking, then the extension command will complete immediately while checkFunc is still running.
|
||||
checkFunc := func(em extensions.ExtensionManager, ext extensions.Extension) (*update.ReleaseInfo, error) {
|
||||
time.Sleep(30 * time.Second)
|
||||
return nil, fmt.Errorf("update check should not have completed")
|
||||
}
|
||||
|
||||
cmd := root.NewCmdExtension(ios, em, ext, checkFunc)
|
||||
|
||||
// The test whether update check is non-blocking is based on how long it takes for the extension command execution.
|
||||
// If there is no wait time as checkFunc is sleeping sufficiently long, we can trust update check is non-blocking.
|
||||
// Otherwise, if any amount of wait is encountered, it is a decent indicator that update checking is blocking.
|
||||
// This is not an ideal test and indicates the update design should be revisited to be easier to understand and manage.
|
||||
completed := make(chan struct{})
|
||||
go func() {
|
||||
_, err := cmd.ExecuteC()
|
||||
require.NoError(t, err)
|
||||
close(completed)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-completed:
|
||||
// Expected behavior assuming extension dispatch exits immediately while checkFunc is still running.
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("extension update check should have exited")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ func ParseRulesForDisplay(rules []RulesetRule) string {
|
|||
for _, rule := range rules {
|
||||
display.WriteString(fmt.Sprintf("- %s", rule.Type))
|
||||
|
||||
if rule.Parameters != nil && len(rule.Parameters) > 0 {
|
||||
if len(rule.Parameters) > 0 {
|
||||
display.WriteString(": ")
|
||||
|
||||
// sort these keys too for consistency
|
||||
|
|
|
|||
|
|
@ -202,6 +202,9 @@ func rerunRun(client *api.Client, repo ghrepo.Interface, run *shared.Run, onlyFa
|
|||
if err != nil {
|
||||
var httpError api.HTTPError
|
||||
if errors.As(err, &httpError) && httpError.StatusCode == 403 {
|
||||
if httpError.Message == "Unable to retry this workflow run because it was created over a month ago" {
|
||||
return fmt.Errorf("run %d cannot be rerun; %s", run.ID, httpError.Message)
|
||||
}
|
||||
return fmt.Errorf("run %d cannot be rerun; its workflow file may be broken", run.ID)
|
||||
}
|
||||
return fmt.Errorf("failed to rerun: %w", err)
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ var ErrMissingAnnotationsPermissions = errors.New("missing annotations permissio
|
|||
|
||||
// GetAnnotations fetches annotations from the REST API.
|
||||
//
|
||||
// If the job has no annotations, an empy slice is returned.
|
||||
// If the job has no annotations, an empty slice is returned.
|
||||
// If the API returns a 403, a custom ErrMissingAnnotationsPermissions error is returned.
|
||||
//
|
||||
// When fine-grained PATs support checks:read permission, we can remove the need for this at the call sites.
|
||||
|
|
|
|||
|
|
@ -46,8 +46,19 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
`),
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
// If the user specified a repo directly, then we're using the OverrideBaseRepoFunc set by EnableRepoOverride
|
||||
// So there's no reason to use the specialised BaseRepoFunc that requires remote disambiguation.
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
if !cmd.Flags().Changed("repo") {
|
||||
// If they haven't specified a repo directly, then we will wrap the BaseRepoFunc in one that errors if
|
||||
// there might be multiple valid remotes.
|
||||
opts.BaseRepo = shared.RequireNoAmbiguityBaseRepoFunc(opts.BaseRepo, f.Remotes)
|
||||
// But if we are able to prompt, then we will wrap that up in a BaseRepoFunc that can prompt the user to
|
||||
// resolve the ambiguity.
|
||||
if opts.IO.CanPrompt() {
|
||||
opts.BaseRepo = shared.PromptWhenAmbiguousBaseRepoFunc(opts.BaseRepo, f.IOStreams, f.Prompter)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil {
|
||||
return err
|
||||
|
|
@ -88,6 +99,14 @@ func removeRun(opts *DeleteOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var baseRepo ghrepo.Interface
|
||||
if secretEntity == shared.Repository || secretEntity == shared.Environment {
|
||||
baseRepo, err = opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
secretApp, err := shared.GetSecretApp(opts.Application, secretEntity)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -97,14 +116,6 @@ func removeRun(opts *DeleteOptions) error {
|
|||
return fmt.Errorf("%s secrets are not supported for %s", secretEntity, secretApp)
|
||||
}
|
||||
|
||||
var baseRepo ghrepo.Interface
|
||||
if secretEntity == shared.Repository || secretEntity == shared.Environment {
|
||||
baseRepo, err = opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -2,12 +2,17 @@ package delete
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
ghContext "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/gh"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/secret/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -120,6 +125,108 @@ func TestNewCmdDelete(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewCmdDeleteBaseRepoFuncs(t *testing.T) {
|
||||
remotes := ghContext.Remotes{
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "fork"),
|
||||
},
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "upstream",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
prompterStubs func(*prompter.MockPrompter)
|
||||
wantRepo ghrepo.Interface
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "when there is a repo flag provided, the factory base repo func is used",
|
||||
args: "SECRET_NAME --repo owner/repo",
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag provided, and no prompting, the base func requiring no ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
wantErr: shared.AmbiguousBaseRepoError{
|
||||
Remotes: remotes,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag provided, and can prompt, the base func resolving ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect(
|
||||
"Select a repo",
|
||||
[]string{"owner/fork", "owner/repo"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "owner/fork")
|
||||
},
|
||||
)
|
||||
},
|
||||
wantRepo: ghrepo.New("owner", "fork"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
var pm *prompter.MockPrompter
|
||||
if tt.prompterStubs != nil {
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
pm = prompter.NewMockPrompter(t)
|
||||
tt.prompterStubs(pm)
|
||||
}
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("owner/repo")
|
||||
},
|
||||
Prompter: pm,
|
||||
Remotes: func() (ghContext.Remotes, error) {
|
||||
return remotes, nil
|
||||
},
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.args)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *DeleteOptions
|
||||
cmd := NewCmdDelete(f, func(opts *DeleteOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
// Require to support --repo flag
|
||||
cmdutil.EnableRepoOverride(cmd, f)
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
require.NoError(t, err)
|
||||
|
||||
baseRepo, err := gotOpts.BaseRepo()
|
||||
if tt.wantErr != nil {
|
||||
require.Equal(t, tt.wantErr, err)
|
||||
return
|
||||
}
|
||||
require.True(t, ghrepo.IsSame(tt.wantRepo, baseRepo))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_removeRun_repo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/secret/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -23,8 +24,10 @@ type ListOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
Config func() (gh.Config, error)
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Now func() time.Time
|
||||
Exporter cmdutil.Exporter
|
||||
Prompter prompter.Prompter
|
||||
|
||||
Now func() time.Time
|
||||
Exporter cmdutil.Exporter
|
||||
|
||||
OrgName string
|
||||
EnvName string
|
||||
|
|
@ -48,6 +51,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
Config: f.Config,
|
||||
HttpClient: f.HttpClient,
|
||||
Now: time.Now,
|
||||
Prompter: f.Prompter,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -63,8 +67,19 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
|
|||
Aliases: []string{"ls"},
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
// If the user specified a repo directly, then we're using the OverrideBaseRepoFunc set by EnableRepoOverride
|
||||
// So there's no reason to use the specialised BaseRepoFunc that requires remote disambiguation.
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
if !cmd.Flags().Changed("repo") {
|
||||
// If they haven't specified a repo directly, then we will wrap the BaseRepoFunc in one that errors if
|
||||
// there might be multiple valid remotes.
|
||||
opts.BaseRepo = shared.RequireNoAmbiguityBaseRepoFunc(opts.BaseRepo, f.Remotes)
|
||||
// But if we are able to prompt, then we will wrap that up in a BaseRepoFunc that can prompt the user to
|
||||
// resolve the ambiguity.
|
||||
if opts.IO.CanPrompt() {
|
||||
opts.BaseRepo = shared.PromptWhenAmbiguousBaseRepoFunc(opts.BaseRepo, f.IOStreams, f.Prompter)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -3,15 +3,19 @@ package list
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ghContext "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/gh"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/secret/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
|
|
@ -101,6 +105,108 @@ func Test_NewCmdList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewCmdListBaseRepoFuncs(t *testing.T) {
|
||||
remotes := ghContext.Remotes{
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "fork"),
|
||||
},
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "upstream",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
prompterStubs func(*prompter.MockPrompter)
|
||||
wantRepo ghrepo.Interface
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "when there is a repo flag provided, the factory base repo func is used",
|
||||
args: "--repo owner/repo",
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag provided, and no prompting, the base func requiring no ambiguity is used",
|
||||
args: "",
|
||||
wantErr: shared.AmbiguousBaseRepoError{
|
||||
Remotes: remotes,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag provided, and can prompt, the base func resolving ambiguity is used",
|
||||
args: "",
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect(
|
||||
"Select a repo",
|
||||
[]string{"owner/fork", "owner/repo"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "owner/fork")
|
||||
},
|
||||
)
|
||||
},
|
||||
wantRepo: ghrepo.New("owner", "fork"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
var pm *prompter.MockPrompter
|
||||
if tt.prompterStubs != nil {
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
pm = prompter.NewMockPrompter(t)
|
||||
tt.prompterStubs(pm)
|
||||
}
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("owner/repo")
|
||||
},
|
||||
Prompter: pm,
|
||||
Remotes: func() (ghContext.Remotes, error) {
|
||||
return remotes, nil
|
||||
},
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.args)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *ListOptions
|
||||
cmd := NewCmdList(f, func(opts *ListOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
// Require to support --repo flag
|
||||
cmdutil.EnableRepoOverride(cmd, f)
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
require.NoError(t, err)
|
||||
|
||||
baseRepo, err := gotOpts.BaseRepo()
|
||||
if tt.wantErr != nil {
|
||||
require.Equal(t, tt.wantErr, err)
|
||||
return
|
||||
}
|
||||
require.True(t, ghrepo.IsSame(tt.wantRepo, baseRepo))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_listRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/gh"
|
||||
|
|
@ -27,7 +29,7 @@ type SetOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
Config func() (gh.Config, error)
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Prompter iprompter
|
||||
Prompter prompter.Prompter
|
||||
|
||||
RandomOverride func() io.Reader
|
||||
|
||||
|
|
@ -43,10 +45,6 @@ type SetOptions struct {
|
|||
Application string
|
||||
}
|
||||
|
||||
type iprompter interface {
|
||||
Password(string) (string, error)
|
||||
}
|
||||
|
||||
func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command {
|
||||
opts := &SetOptions{
|
||||
IO: f.IOStreams,
|
||||
|
|
@ -77,6 +75,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
# Read secret value from an environment variable
|
||||
$ gh secret set MYSECRET --body "$ENV_VALUE"
|
||||
|
||||
# Set secret for a specific remote repository
|
||||
$ gh secret set MYSECRET --repo origin/repo --body "$ENV_VALUE"
|
||||
|
||||
# Read secret value from a file
|
||||
$ gh secret set MYSECRET < myfile.txt
|
||||
|
||||
|
|
@ -103,8 +104,19 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
// If the user specified a repo directly, then we're using the OverrideBaseRepoFunc set by EnableRepoOverride
|
||||
// So there's no reason to use the specialised BaseRepoFunc that requires remote disambiguation.
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
if !cmd.Flags().Changed("repo") {
|
||||
// If they haven't specified a repo directly, then we will wrap the BaseRepoFunc in one that errors if
|
||||
// there might be multiple valid remotes.
|
||||
opts.BaseRepo = shared.RequireNoAmbiguityBaseRepoFunc(opts.BaseRepo, f.Remotes)
|
||||
// But if we are able to prompt, then we will wrap that up in a BaseRepoFunc that can prompt the user to
|
||||
// resolve the ambiguity.
|
||||
if opts.IO.CanPrompt() {
|
||||
opts.BaseRepo = shared.PromptWhenAmbiguousBaseRepoFunc(opts.BaseRepo, f.IOStreams, f.Prompter)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmdutil.MutuallyExclusive("specify only one of `--org`, `--env`, or `--user`", opts.OrgName != "", opts.EnvName != "", opts.UserSecrets); err != nil {
|
||||
return err
|
||||
|
|
@ -166,6 +178,27 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
}
|
||||
|
||||
func setRun(opts *SetOptions) error {
|
||||
orgName := opts.OrgName
|
||||
envName := opts.EnvName
|
||||
|
||||
var host string
|
||||
var baseRepo ghrepo.Interface
|
||||
if orgName == "" && !opts.UserSecrets {
|
||||
var err error
|
||||
baseRepo, err = opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host = baseRepo.RepoHost()
|
||||
} else {
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
host, _ = cfg.Authentication().DefaultHost()
|
||||
}
|
||||
|
||||
secrets, err := getSecretsFromOptions(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -177,25 +210,6 @@ func setRun(opts *SetOptions) error {
|
|||
}
|
||||
client := api.NewClientFromHTTP(c)
|
||||
|
||||
orgName := opts.OrgName
|
||||
envName := opts.EnvName
|
||||
|
||||
var host string
|
||||
var baseRepo ghrepo.Interface
|
||||
if orgName == "" && !opts.UserSecrets {
|
||||
baseRepo, err = opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
host = baseRepo.RepoHost()
|
||||
} else {
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
host, _ = cfg.Authentication().DefaultHost()
|
||||
}
|
||||
|
||||
secretEntity, err := shared.GetSecretEntity(orgName, envName, opts.UserSecrets)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
ghContext "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/gh"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
|
|
@ -20,6 +22,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdSet(t *testing.T) {
|
||||
|
|
@ -221,6 +224,108 @@ func TestNewCmdSet(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
|
||||
remotes := ghContext.Remotes{
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "fork"),
|
||||
},
|
||||
&ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "upstream",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
prompterStubs func(*prompter.MockPrompter)
|
||||
wantRepo ghrepo.Interface
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "when there is a repo flag provided, the factory base repo func is used",
|
||||
args: "SECRET_NAME --repo owner/repo",
|
||||
wantRepo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag provided, and no prompting, the base func requiring no ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
wantErr: shared.AmbiguousBaseRepoError{
|
||||
Remotes: remotes,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when there is no repo flag provided, and can prompt, the base func resolving ambiguity is used",
|
||||
args: "SECRET_NAME",
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect(
|
||||
"Select a repo",
|
||||
[]string{"owner/fork", "owner/repo"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "owner/fork")
|
||||
},
|
||||
)
|
||||
},
|
||||
wantRepo: ghrepo.New("owner", "fork"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
var pm *prompter.MockPrompter
|
||||
if tt.prompterStubs != nil {
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStdoutTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
pm = prompter.NewMockPrompter(t)
|
||||
tt.prompterStubs(pm)
|
||||
}
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("owner/repo")
|
||||
},
|
||||
Prompter: pm,
|
||||
Remotes: func() (ghContext.Remotes, error) {
|
||||
return remotes, nil
|
||||
},
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.args)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts *SetOptions
|
||||
cmd := NewCmdSet(f, func(opts *SetOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
// Require to support --repo flag
|
||||
cmdutil.EnableRepoOverride(cmd, f)
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
require.NoError(t, err)
|
||||
|
||||
baseRepo, err := gotOpts.BaseRepo()
|
||||
if tt.wantErr != nil {
|
||||
require.Equal(t, tt.wantErr, err)
|
||||
return
|
||||
}
|
||||
require.True(t, ghrepo.IsSame(tt.wantRepo, baseRepo))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_setRun_repo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
71
pkg/cmd/secret/shared/base_repo.go
Normal file
71
pkg/cmd/secret/shared/base_repo.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
ghContext "github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
)
|
||||
|
||||
type AmbiguousBaseRepoError struct {
|
||||
Remotes ghContext.Remotes
|
||||
}
|
||||
|
||||
func (e AmbiguousBaseRepoError) Error() string {
|
||||
return "multiple remotes detected. please specify which repo to use by providing the -R, --repo argument"
|
||||
}
|
||||
|
||||
type baseRepoFn func() (ghrepo.Interface, error)
|
||||
type remotesFn func() (ghContext.Remotes, error)
|
||||
|
||||
func PromptWhenAmbiguousBaseRepoFunc(baseRepoFn baseRepoFn, ios *iostreams.IOStreams, prompter prompter.Prompter) baseRepoFn {
|
||||
return func() (ghrepo.Interface, error) {
|
||||
baseRepo, err := baseRepoFn()
|
||||
if err != nil {
|
||||
var ambiguousBaseRepoErr AmbiguousBaseRepoError
|
||||
if !errors.As(err, &ambiguousBaseRepoErr) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseRepoOptions := make([]string, len(ambiguousBaseRepoErr.Remotes))
|
||||
for i, remote := range ambiguousBaseRepoErr.Remotes {
|
||||
baseRepoOptions[i] = ghrepo.FullName(remote)
|
||||
}
|
||||
|
||||
fmt.Fprintf(ios.Out, "%s Multiple remotes detected. Due to the sensitive nature of secrets, requiring disambiguation.\n", ios.ColorScheme().WarningIcon())
|
||||
selectedBaseRepo, err := prompter.Select("Select a repo", baseRepoOptions[0], baseRepoOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selectedRepo, err := ghrepo.FromFullName(baseRepoOptions[selectedBaseRepo])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return selectedRepo, nil
|
||||
}
|
||||
|
||||
return baseRepo, nil
|
||||
}
|
||||
}
|
||||
|
||||
// RequireNoAmbiguityBaseRepoFunc returns a function to resolve the base repo, ensuring that
|
||||
// there was only one option, regardless of whether the base repo had been set.
|
||||
func RequireNoAmbiguityBaseRepoFunc(baseRepo baseRepoFn, remotes remotesFn) baseRepoFn {
|
||||
return func() (ghrepo.Interface, error) {
|
||||
remotes, err := remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if remotes.Len() > 1 {
|
||||
return nil, AmbiguousBaseRepoError{Remotes: remotes}
|
||||
}
|
||||
|
||||
return baseRepo()
|
||||
}
|
||||
}
|
||||
249
pkg/cmd/secret/shared/base_repo_test.go
Normal file
249
pkg/cmd/secret/shared/base_repo_test.go
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
package shared_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
ghContext "github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/pkg/cmd/secret/shared"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
)
|
||||
|
||||
func TestRequireNoAmbiguityBaseRepoFunc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("succeeds when there is only one remote", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given there is only one remote
|
||||
baseRepoFn := shared.RequireNoAmbiguityBaseRepoFunc(baseRepoStubFn, oneRemoteStubFn)
|
||||
|
||||
// When fetching the base repo
|
||||
baseRepo, err := baseRepoFn()
|
||||
|
||||
// It succeeds and returns the inner base repo
|
||||
require.NoError(t, err)
|
||||
require.True(t, ghrepo.IsSame(ghrepo.New("owner", "repo"), baseRepo))
|
||||
})
|
||||
|
||||
t.Run("returns specific error when there are multiple remotes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given there are multiple remotes
|
||||
baseRepoFn := shared.RequireNoAmbiguityBaseRepoFunc(baseRepoStubFn, twoRemotesStubFn)
|
||||
|
||||
// When fetching the base repo
|
||||
_, err := baseRepoFn()
|
||||
|
||||
// It succeeds and returns the inner base repo
|
||||
var multipleRemotesError shared.AmbiguousBaseRepoError
|
||||
require.ErrorAs(t, err, &multipleRemotesError)
|
||||
require.Equal(t, ghContext.Remotes{
|
||||
{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "fork"),
|
||||
},
|
||||
{
|
||||
Remote: &git.Remote{
|
||||
Name: "upstream",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "repo"),
|
||||
},
|
||||
}, multipleRemotesError.Remotes)
|
||||
})
|
||||
|
||||
t.Run("when the remote fetching function fails, it returns the error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given the remote fetching function fails
|
||||
baseRepoFn := shared.RequireNoAmbiguityBaseRepoFunc(baseRepoStubFn, errRemoteStubFn)
|
||||
|
||||
// When fetching the base repo
|
||||
_, err := baseRepoFn()
|
||||
|
||||
// It returns the error
|
||||
require.Equal(t, errors.New("test remote error"), err)
|
||||
})
|
||||
|
||||
t.Run("when the wrapped base repo function fails, it returns the error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given the wrapped base repo function fails
|
||||
baseRepoFn := shared.RequireNoAmbiguityBaseRepoFunc(errBaseRepoStubFn, oneRemoteStubFn)
|
||||
|
||||
// When fetching the base repo
|
||||
_, err := baseRepoFn()
|
||||
|
||||
// It returns the error
|
||||
require.Equal(t, errors.New("test base repo error"), err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPromptWhenMultipleRemotesBaseRepoFunc(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("when there is no error from wrapped base repo func, then it succeeds without prompting", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
// Given the base repo function succeeds
|
||||
baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(baseRepoStubFn, ios, nil)
|
||||
|
||||
// When fetching the base repo
|
||||
baseRepo, err := baseRepoFn()
|
||||
|
||||
// It succeeds and returns the inner base repo
|
||||
require.NoError(t, err)
|
||||
require.True(t, ghrepo.IsSame(ghrepo.New("owner", "repo"), baseRepo))
|
||||
})
|
||||
|
||||
t.Run("when the wrapped base repo func returns a specific error, then the prompter is used for disambiguation, with the remote ordering remaining unchanged", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
pm.RegisterSelect(
|
||||
"Select a repo",
|
||||
[]string{"owner/fork", "owner/repo"},
|
||||
func(_, def string, opts []string) (int, error) {
|
||||
require.Equal(t, "owner/fork", def)
|
||||
return prompter.IndexFor(opts, "owner/repo")
|
||||
},
|
||||
)
|
||||
|
||||
// Given the wrapped base repo func returns a specific error
|
||||
baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errMultipleRemotesStubFn, ios, pm)
|
||||
|
||||
// When fetching the base repo
|
||||
baseRepo, err := baseRepoFn()
|
||||
|
||||
// It prints an informative message
|
||||
require.Equal(t, "! Multiple remotes detected. Due to the sensitive nature of secrets, requiring disambiguation.\n", stdout.String())
|
||||
|
||||
// And it uses the prompter for disambiguation
|
||||
require.NoError(t, err)
|
||||
require.True(t, ghrepo.IsSame(ghrepo.New("owner", "repo"), baseRepo))
|
||||
})
|
||||
|
||||
t.Run("when the prompter returns an error, then it is returned", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
// Given the prompter returns an error
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
pm.RegisterSelect(
|
||||
"Select a repo",
|
||||
[]string{"owner/fork", "owner/repo"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return 0, errors.New("test prompt error")
|
||||
},
|
||||
)
|
||||
|
||||
// Given the wrapped base repo func returns a specific error
|
||||
baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errMultipleRemotesStubFn, ios, pm)
|
||||
|
||||
// When fetching the base repo
|
||||
_, err := baseRepoFn()
|
||||
|
||||
// It returns the error
|
||||
require.Equal(t, errors.New("test prompt error"), err)
|
||||
})
|
||||
|
||||
t.Run("when the wrapped base repo func returns a non-specific error, then it is returned", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
// Given the wrapped base repo func returns a non-specific error
|
||||
baseRepoFn := shared.PromptWhenAmbiguousBaseRepoFunc(errBaseRepoStubFn, ios, nil)
|
||||
|
||||
// When fetching the base repo
|
||||
_, err := baseRepoFn()
|
||||
|
||||
// It returns the error
|
||||
require.Equal(t, errors.New("test base repo error"), err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultipleRemotesErrorMessage(t *testing.T) {
|
||||
err := shared.AmbiguousBaseRepoError{}
|
||||
require.EqualError(t, err, "multiple remotes detected. please specify which repo to use by providing the -R, --repo argument")
|
||||
}
|
||||
|
||||
func errMultipleRemotesStubFn() (ghrepo.Interface, error) {
|
||||
remote1 := &ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "fork"),
|
||||
}
|
||||
|
||||
remote2 := &ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "upstream",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "repo"),
|
||||
}
|
||||
|
||||
return nil, shared.AmbiguousBaseRepoError{
|
||||
Remotes: ghContext.Remotes{
|
||||
remote1,
|
||||
remote2,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func baseRepoStubFn() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("owner", "repo"), nil
|
||||
}
|
||||
|
||||
func oneRemoteStubFn() (ghContext.Remotes, error) {
|
||||
remote := &ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "repo"),
|
||||
}
|
||||
|
||||
return ghContext.Remotes{
|
||||
remote,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func twoRemotesStubFn() (ghContext.Remotes, error) {
|
||||
remote1 := &ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "origin",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "fork"),
|
||||
}
|
||||
|
||||
remote2 := &ghContext.Remote{
|
||||
Remote: &git.Remote{
|
||||
Name: "upstream",
|
||||
},
|
||||
Repo: ghrepo.New("owner", "repo"),
|
||||
}
|
||||
return ghContext.Remotes{
|
||||
remote1,
|
||||
remote2,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func errRemoteStubFn() (ghContext.Remotes, error) {
|
||||
return nil, errors.New("test remote error")
|
||||
}
|
||||
|
||||
func errBaseRepoStubFn() (ghrepo.Interface, error) {
|
||||
return nil, errors.New("test base repo error")
|
||||
}
|
||||
|
|
@ -254,7 +254,7 @@ func (e *jsonExporter) exportData(v reflect.Value) interface{} {
|
|||
}
|
||||
return m.Interface()
|
||||
case reflect.Struct:
|
||||
if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(exportableType) {
|
||||
if v.CanAddr() && reflect.PointerTo(v.Type()).Implements(exportableType) {
|
||||
ve := v.Addr().Interface().(exportable)
|
||||
return ve.ExportData(e.fields)
|
||||
} else if v.Type().Implements(exportableType) {
|
||||
|
|
|
|||
|
|
@ -524,7 +524,7 @@ func (w *fdWriteCloser) Fd() uintptr {
|
|||
return w.fd
|
||||
}
|
||||
|
||||
// fdWriter represents a wrapped stdin ReadCloser that preserves the original file descriptor
|
||||
// fdReader represents a wrapped stdin ReadCloser that preserves the original file descriptor
|
||||
type fdReader struct {
|
||||
io.ReadCloser
|
||||
fd uintptr
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue