Merge branch 'trunk' into refactor-get-attestations-funcs
This commit is contained in:
commit
4daf489c75
28 changed files with 710 additions and 83 deletions
27
.github/CONTRIBUTING.md
vendored
27
.github/CONTRIBUTING.md
vendored
|
|
@ -4,23 +4,22 @@ Hi! Thanks for your interest in contributing to the GitHub CLI!
|
|||
|
||||
We accept pull requests for bug fixes and features where we've discussed the approach in an issue and given the go-ahead for a community member to work on it. We'd also love to hear about ideas for new features as issues.
|
||||
|
||||
Please do:
|
||||
### Please do:
|
||||
|
||||
* Check issues to verify that a [bug][bug issues] or [feature request][feature request issues] issue does not already exist for the same problem or feature.
|
||||
* Open an issue if things aren't working as expected.
|
||||
* Open an issue to propose a significant change.
|
||||
* Open an issue to propose a design for an issue labelled [`needs-design` and `help wanted`][needs design and help wanted], following the [proposing a design guidelines](#proposing-a-design) instructions below.
|
||||
* Open a pull request to fix a bug.
|
||||
* Open a pull request to fix documentation about a command.
|
||||
* Open a pull request for any issue labelled [`help wanted`][hw] or [`good first issue`][gfi].
|
||||
* Check issues to verify that a [bug][bug issues] or [feature request][feature request issues] issue does not already exist for the same problem or feature
|
||||
* Open an issue if things aren't working as expected
|
||||
* Open an issue to propose a significant change
|
||||
* Open an issue to propose a design for an issue labelled [`needs-design` and `help wanted`][needs design and help wanted], following the [proposing a design guidelines](#proposing-a-design) instructions below
|
||||
* Mention `@cli/code-reviewers` when an issue you want to work on does not have clear Acceptance Criteria
|
||||
* Open a pull request for any issue labelled [`help wanted`][hw] and [`good first issue`][gfi]
|
||||
|
||||
Please avoid:
|
||||
### Please _do not_:
|
||||
|
||||
* Opening pull requests for issues marked `needs-design`, `needs-investigation`, or `blocked`.
|
||||
* Opening pull requests that haven't been approved for work in an issue
|
||||
* Adding installation instructions specifically for your OS/package manager.
|
||||
* Opening pull requests for any issue marked `core`. These issues require additional context from
|
||||
the core CLI team at GitHub and any external pull requests will not be accepted.
|
||||
* Open a pull request for issues without the `help wanted` label or explicit Acceptance Criteria
|
||||
* Expand pull request scope to include changes that are not described in the issue's Acceptance Criteria
|
||||
* Add installation instructions specifically for your OS/package manager
|
||||
* Open pull requests for any issue marked `core`. These issues require additional context from
|
||||
the core CLI team at GitHub and any external pull requests will not be accepted
|
||||
|
||||
## Building the project
|
||||
|
||||
|
|
|
|||
|
|
@ -1,39 +1,52 @@
|
|||
# Releasing
|
||||
|
||||
To initiate a new production deployment:
|
||||
|
||||
```sh
|
||||
script/release vX.Y.Z
|
||||
```
|
||||
|
||||
See `script/release --help` for more information.
|
||||
|
||||
> **Note:**
|
||||
> Every production release will request an approval by the select few people before it can proceed.
|
||||
> [!NOTE]
|
||||
> Deployment workflow requires maintainer approval to run.
|
||||
|
||||
What this does is:
|
||||
|
||||
- Builds Linux binaries on Ubuntu;
|
||||
- Builds and signs Windows binaries on Windows;
|
||||
- Builds, signs, and notarizes macOS binaries on macOS;
|
||||
- Uploads all release artifacts to a new GitHub Release;
|
||||
- A new git tag `vX.Y.Z` is created in the remote repository;
|
||||
- The changelog is [generated from the list of merged pull requests](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes);
|
||||
- Updates cli.github.com with the contents of the new release;
|
||||
- Updates [GitHub CLI marketing site](https://cli.github.com) with the contents of the new release;
|
||||
- Updates the [`gh` Homebrew formula](https://github.com/williammartin/homebrew-core/blob/master/Formula/g/gh.rb) in the [`homebrew/homebrew-core` repo](https://github.com/search?q=repo%3AHomebrew%2Fhomebrew-core+%22gh%22+in%3Atitle&type=pullrequests).
|
||||
|
||||
> [!NOTE]
|
||||
> `Homebrew/formulae.brew.sh` makes new formula versions available every 15 minutes through scheduled [CI workflow](https://github.com/Homebrew/formulae.brew.sh/actions/workflows/tests.yml).
|
||||
>
|
||||
> For more information, see https://docs.brew.sh/Formula-Cookbook#an-introduction
|
||||
|
||||
To test out the build system while avoiding creating an actual release:
|
||||
|
||||
```sh
|
||||
script/release --staging vX.Y.Z --branch patch-1 -p macos
|
||||
```
|
||||
|
||||
The build artifacts will be available via `gh run download <RUN> -n macos`.
|
||||
|
||||
## General guidelines
|
||||
|
||||
* Features to be released should be reviewed and approved at least one day prior to the release.
|
||||
* Feature releases should bump up the minor version number.
|
||||
* Breaking releases should bump up the major version number. These should generally be rare.
|
||||
- Features to be released should be reviewed and approved at least one day prior
|
||||
to the release.
|
||||
- Feature releases should bump up the minor version number.
|
||||
- Breaking releases should bump up the major version number. These should
|
||||
generally be rare.
|
||||
|
||||
## Test the build system locally
|
||||
|
||||
A local release can be created for testing without creating anything official on the release page.
|
||||
A local release can be created for testing without creating anything official on
|
||||
the release page.
|
||||
|
||||
1. Make sure GoReleaser is installed: `brew install goreleaser`
|
||||
2. `script/release --local`
|
||||
|
|
@ -45,5 +58,6 @@ Occasionally, it might be necessary to clean up a bad release and re-release.
|
|||
|
||||
1. Delete the release and associated tag
|
||||
2. Re-release and monitor the workflow run logs
|
||||
3. Open pull request updating [`gh` Homebrew formula](https://github.com/williammartin/homebrew-core/blob/master/Formula/g/gh.rb) with new SHA versions, linking the previous PR
|
||||
3. Open pull request updating [`gh` Homebrew formula](https://github.com/williammartin/homebrew-core/blob/master/Formula/g/gh.rb)
|
||||
with new SHA versions, linking the previous PR
|
||||
4. Verify resulting Debian and RPM packages, Homebrew formula
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -51,7 +51,7 @@ require (
|
|||
golang.org/x/term v0.28.0
|
||||
golang.org/x/text v0.21.0
|
||||
google.golang.org/grpc v1.69.4
|
||||
google.golang.org/protobuf v1.36.4
|
||||
google.golang.org/protobuf v1.36.5
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -559,8 +559,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:
|
|||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
|
||||
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
||||
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/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=
|
||||
|
|
|
|||
|
|
@ -463,9 +463,11 @@ func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersW
|
|||
|
||||
var serverError string
|
||||
if isJSON && (opts.RequestPath == "graphql" || resp.StatusCode >= 400) {
|
||||
responseBody, serverError, err = parseErrorResponse(responseBody, resp.StatusCode)
|
||||
if err != nil {
|
||||
return
|
||||
if !strings.EqualFold(opts.RequestMethod, "HEAD") {
|
||||
responseBody, serverError, err = parseErrorResponse(responseBody, resp.StatusCode)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1234,6 +1234,35 @@ func Test_apiRun_DELETE(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_apiRun_HEAD(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
err := apiRun(&ApiOptions{
|
||||
IO: ios,
|
||||
Config: func() (gh.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
var tr roundTripper = func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 422,
|
||||
Request: req,
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {"application/json"},
|
||||
}}, nil
|
||||
}
|
||||
return &http.Client{Transport: tr}, nil
|
||||
},
|
||||
MagicFields: []string(nil),
|
||||
RawFields: []string(nil),
|
||||
RequestMethod: "HEAD",
|
||||
RequestMethodPassed: true,
|
||||
})
|
||||
if err != cmdutil.SilentError {
|
||||
t.Fatalf("got error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_apiRun_inputFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ func runVerify(opts *Options) error {
|
|||
filteredAttestations := verification.FilterAttestations(ec.PredicateType, attestations)
|
||||
if len(filteredAttestations) == 0 {
|
||||
opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ No attestations found with predicate type: %s\n"), opts.PredicateType)
|
||||
return err
|
||||
return fmt.Errorf("no matching predicate found")
|
||||
}
|
||||
attestations = filteredAttestations
|
||||
|
||||
|
|
|
|||
|
|
@ -501,6 +501,18 @@ func TestRunVerify(t *testing.T) {
|
|||
require.Nil(t, runVerify(&customOpts))
|
||||
})
|
||||
|
||||
t.Run("with valid OCI artifact with UseBundleFromRegistry flag and unknown predicate type", func(t *testing.T) {
|
||||
customOpts := publicGoodOpts
|
||||
customOpts.ArtifactPath = "oci://ghcr.io/github/test"
|
||||
customOpts.BundlePath = ""
|
||||
customOpts.UseBundleFromRegistry = true
|
||||
customOpts.PredicateType = "https://predicate.type"
|
||||
|
||||
err := runVerify(&customOpts)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "no matching predicate found")
|
||||
})
|
||||
|
||||
t.Run("with valid OCI artifact with UseBundleFromRegistry flag but no bundle return from registry", func(t *testing.T) {
|
||||
customOpts := publicGoodOpts
|
||||
customOpts.ArtifactPath = "oci://ghcr.io/github/test"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
ghContext "github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
|
|
@ -51,7 +52,15 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
|
|||
cmd := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show status of relevant pull requests",
|
||||
Args: cmdutil.NoArgsQuoteReminder,
|
||||
Long: heredoc.Docf(`
|
||||
Show status of relevant pull requests.
|
||||
|
||||
The status shows a summary of pull requests that includes information such as
|
||||
pull request number, title, CI checks, reviews, etc.
|
||||
|
||||
To see more details of CI checks, run %[1]sgh pr checks%[1]s.
|
||||
`, "`"),
|
||||
Args: cmdutil.NoArgsQuoteReminder,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ type editItemOpts struct {
|
|||
fieldID string
|
||||
projectID string
|
||||
text string
|
||||
number float32
|
||||
number float64
|
||||
date string
|
||||
singleSelectOptionID string
|
||||
iterationID string
|
||||
|
|
@ -123,7 +123,7 @@ func NewCmdEditItem(f *cmdutil.Factory, runF func(config editItemConfig) error)
|
|||
editItemCmd.Flags().StringVar(&opts.fieldID, "field-id", "", "ID of the field to update")
|
||||
editItemCmd.Flags().StringVar(&opts.projectID, "project-id", "", "ID of the project to which the field belongs to")
|
||||
editItemCmd.Flags().StringVar(&opts.text, "text", "", "Text value for the field")
|
||||
editItemCmd.Flags().Float32Var(&opts.number, "number", 0, "Number value for the field")
|
||||
editItemCmd.Flags().Float64Var(&opts.number, "number", 0, "Number value for the field")
|
||||
editItemCmd.Flags().StringVar(&opts.date, "date", "", "Date value for the field (YYYY-MM-DD)")
|
||||
editItemCmd.Flags().StringVar(&opts.singleSelectOptionID, "single-select-option-id", "", "ID of the single select option value to set on the field")
|
||||
editItemCmd.Flags().StringVar(&opts.iterationID, "iteration-id", "", "ID of the iteration value to set on the field")
|
||||
|
|
|
|||
|
|
@ -47,6 +47,14 @@ func TestNewCmdeditItem(t *testing.T) {
|
|||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "number with floating point value",
|
||||
cli: "--number 123.45 --id 123",
|
||||
wants: editItemOpts{
|
||||
number: 123.45,
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "field-id",
|
||||
cli: "--field-id FIELD_ID --id 123",
|
||||
|
|
@ -255,7 +263,7 @@ func TestRunItemEdit_Number(t *testing.T) {
|
|||
// edit item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"number":2}}}}`).
|
||||
BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"number":123.45}}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
|
|
@ -284,7 +292,7 @@ func TestRunItemEdit_Number(t *testing.T) {
|
|||
config := editItemConfig{
|
||||
io: ios,
|
||||
opts: editItemOpts{
|
||||
number: 2,
|
||||
number: 123.45,
|
||||
itemID: "item_id",
|
||||
projectID: "project_id",
|
||||
fieldID: "field_id",
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ type FieldValueNodes struct {
|
|||
Field ProjectField
|
||||
} `graphql:"... on ProjectV2ItemFieldLabelValue"`
|
||||
ProjectV2ItemFieldNumberValue struct {
|
||||
Number float32
|
||||
Number float64
|
||||
Field ProjectField
|
||||
} `graphql:"... on ProjectV2ItemFieldNumberValue"`
|
||||
ProjectV2ItemFieldSingleSelectValue struct {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package autolink
|
|||
import (
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
cmdCreate "github.com/cli/cli/v2/pkg/cmd/repo/autolink/create"
|
||||
cmdDelete "github.com/cli/cli/v2/pkg/cmd/repo/autolink/delete"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/repo/autolink/list"
|
||||
cmdView "github.com/cli/cli/v2/pkg/cmd/repo/autolink/view"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -26,6 +27,7 @@ func NewCmdAutolink(f *cmdutil.Factory) *cobra.Command {
|
|||
cmd.AddCommand(cmdList.NewCmdList(f, nil))
|
||||
cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil))
|
||||
cmd.AddCommand(cmdView.NewCmdView(f, nil))
|
||||
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ func createRun(opts *createOptions) error {
|
|||
"%s Created repository autolink %s on %s\n",
|
||||
cs.SuccessIconWithColor(cs.Green),
|
||||
cs.Cyanf("%d", autolink.ID),
|
||||
ghrepo.FullName(repo))
|
||||
cs.Bold(ghrepo.FullName(repo)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,11 +92,11 @@ func TestNewCmdCreate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type stubAutoLinkCreator struct {
|
||||
type stubAutolinkCreator struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (g stubAutoLinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error) {
|
||||
func (g stubAutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRequest) (*shared.Autolink, error) {
|
||||
if g.err != nil {
|
||||
return nil, g.err
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ func TestCreateRun(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
opts *createOptions
|
||||
stubCreator stubAutoLinkCreator
|
||||
stubCreator stubAutolinkCreator
|
||||
expectedErr error
|
||||
errMsg string
|
||||
wantStdout string
|
||||
|
|
@ -131,7 +131,7 @@ func TestCreateRun(t *testing.T) {
|
|||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
},
|
||||
stubCreator: stubAutoLinkCreator{},
|
||||
stubCreator: stubAutolinkCreator{},
|
||||
wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
|
|
@ -141,7 +141,7 @@ func TestCreateRun(t *testing.T) {
|
|||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
Numeric: true,
|
||||
},
|
||||
stubCreator: stubAutoLinkCreator{},
|
||||
stubCreator: stubAutolinkCreator{},
|
||||
wantStdout: "✓ Created repository autolink 1 on OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
|
|
@ -150,7 +150,7 @@ func TestCreateRun(t *testing.T) {
|
|||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
},
|
||||
stubCreator: stubAutoLinkCreator{err: testAutolinkClientCreateError{}},
|
||||
stubCreator: stubAutolinkCreator{err: testAutolinkClientCreateError{}},
|
||||
expectedErr: testAutolinkClientCreateError{},
|
||||
errMsg: fmt.Sprint("error creating autolink: ", testAutolinkClientCreateError{}.Error()),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,10 +45,6 @@ func (a *AutolinkCreator) Create(repo ghrepo.Interface, request AutolinkCreateRe
|
|||
|
||||
defer resp.Body.Close()
|
||||
|
||||
// if resp.StatusCode != http.StatusCreated {
|
||||
// return nil, api.HandleHTTPError(resp)
|
||||
// }
|
||||
|
||||
err = handleAutolinkCreateError(resp)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAutoLinkCreator_Create(t *testing.T) {
|
||||
func TestAutolinkCreator_Create(t *testing.T) {
|
||||
repo := ghrepo.New("OWNER", "REPO")
|
||||
|
||||
tests := []struct {
|
||||
|
|
|
|||
107
pkg/cmd/repo/autolink/delete/delete.go
Normal file
107
pkg/cmd/repo/autolink/delete/delete.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/view"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type deleteOptions struct {
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser.Browser
|
||||
AutolinkDeleteClient AutolinkDeleteClient
|
||||
AutolinkViewClient view.AutolinkViewClient
|
||||
IO *iostreams.IOStreams
|
||||
|
||||
ID string
|
||||
Confirmed bool
|
||||
Prompter prompter.Prompter
|
||||
}
|
||||
|
||||
type AutolinkDeleteClient interface {
|
||||
Delete(repo ghrepo.Interface, id string) error
|
||||
}
|
||||
|
||||
func NewCmdDelete(f *cmdutil.Factory, runF func(*deleteOptions) error) *cobra.Command {
|
||||
opts := &deleteOptions{
|
||||
Browser: f.Browser,
|
||||
IO: f.IOStreams,
|
||||
Prompter: f.Prompter,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete <id>",
|
||||
Short: "Delete an autolink reference",
|
||||
Long: "Delete an autolink reference for a repository.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.AutolinkDeleteClient = &AutolinkDeleter{HTTPClient: httpClient}
|
||||
opts.AutolinkViewClient = &view.AutolinkViewer{HTTPClient: httpClient}
|
||||
opts.ID = args[0]
|
||||
|
||||
if !opts.IO.CanPrompt() && !opts.Confirmed {
|
||||
return cmdutil.FlagErrorf("--yes required when not running interactively")
|
||||
}
|
||||
|
||||
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 {
|
||||
repo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out := opts.IO.Out
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
autolink, err := opts.AutolinkViewClient.View(repo, opts.ID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %w", cs.Red("error deleting autolink:"), err)
|
||||
}
|
||||
|
||||
if opts.IO.CanPrompt() && !opts.Confirmed {
|
||||
fmt.Fprintf(out, "Autolink %s has key prefix %s.\n", cs.Cyan(opts.ID), autolink.KeyPrefix)
|
||||
|
||||
err := opts.Prompter.ConfirmDeletion(autolink.KeyPrefix)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = opts.AutolinkDeleteClient.Delete(repo, opts.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(out, "%s Autolink %s deleted from %s\n", cs.SuccessIcon(), cs.Cyan(opts.ID), cs.Bold(ghrepo.FullName(repo)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
335
pkg/cmd/repo/autolink/delete/delete_test.go
Normal file
335
pkg/cmd/repo/autolink/delete/delete_test.go
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/browser"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/repo/autolink/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
output deleteOptions
|
||||
isTTY bool
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "no argument",
|
||||
input: "",
|
||||
isTTY: true,
|
||||
wantErr: true,
|
||||
errMsg: "accepts 1 arg(s), received 0",
|
||||
},
|
||||
{
|
||||
name: "id provided",
|
||||
input: "123",
|
||||
isTTY: true,
|
||||
output: deleteOptions{ID: "123"},
|
||||
},
|
||||
{
|
||||
name: "yes flag",
|
||||
input: "123 --yes",
|
||||
isTTY: true,
|
||||
output: deleteOptions{ID: "123", Confirmed: true},
|
||||
},
|
||||
{
|
||||
name: "non-TTY",
|
||||
input: "123 --yes",
|
||||
isTTY: false,
|
||||
output: deleteOptions{ID: "123", Confirmed: true},
|
||||
},
|
||||
{
|
||||
name: "non-TTY missing yes flag",
|
||||
input: "123",
|
||||
isTTY: false,
|
||||
wantErr: true,
|
||||
errMsg: "--yes required when not running interactively",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
ios.SetStdinTTY(tt.isTTY)
|
||||
ios.SetStdoutTTY(tt.isTTY)
|
||||
ios.SetStderrTTY(tt.isTTY)
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
f.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{}, nil
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.input)
|
||||
require.NoError(t, err)
|
||||
|
||||
var gotOpts *deleteOptions
|
||||
cmd := NewCmdDelete(f, func(opts *deleteOptions) error {
|
||||
gotOpts = opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantErr {
|
||||
require.EqualError(t, err, tt.errMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.output.ID, gotOpts.ID)
|
||||
assert.Equal(t, tt.output.Confirmed, gotOpts.Confirmed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubAutolinkDeleter struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (d *stubAutolinkDeleter) Delete(repo ghrepo.Interface, id string) error {
|
||||
return d.err
|
||||
}
|
||||
|
||||
type stubAutolinkViewer struct {
|
||||
autolink *shared.Autolink
|
||||
err error
|
||||
}
|
||||
|
||||
func (g stubAutolinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, error) {
|
||||
return g.autolink, g.err
|
||||
}
|
||||
|
||||
var errTestPrompt = errors.New("prompt error")
|
||||
var errTestAutolinkClientView = errors.New("autolink client view error")
|
||||
var errTestAutolinkClientDelete = errors.New("autolink client delete error")
|
||||
|
||||
func TestDeleteRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *deleteOptions
|
||||
isTTY bool
|
||||
stubDeleter stubAutolinkDeleter
|
||||
stubViewer stubAutolinkViewer
|
||||
prompterStubs func(*prompter.PrompterMock)
|
||||
|
||||
wantStdout string
|
||||
expectedErr error
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "delete",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
},
|
||||
isTTY: true,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{
|
||||
err: nil,
|
||||
},
|
||||
prompterStubs: func(pm *prompter.PrompterMock) {
|
||||
pm.ConfirmDeletionFunc = func(_ string) error {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
wantStdout: heredoc.Doc(`
|
||||
Autolink 123 has key prefix TICKET-.
|
||||
✓ Autolink 123 deleted from OWNER/REPO
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "delete with confirm flag",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
Confirmed: true,
|
||||
},
|
||||
isTTY: true,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{},
|
||||
wantStdout: "✓ Autolink 123 deleted from OWNER/REPO\n",
|
||||
},
|
||||
{
|
||||
name: "confirmation fails",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
},
|
||||
isTTY: true,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{},
|
||||
prompterStubs: func(pm *prompter.PrompterMock) {
|
||||
pm.ConfirmDeletionFunc = func(_ string) error {
|
||||
return errTestPrompt
|
||||
}
|
||||
},
|
||||
expectedErr: errTestPrompt,
|
||||
expectedErrMsg: errTestPrompt.Error(),
|
||||
wantStdout: "Autolink 123 has key prefix TICKET-.\n",
|
||||
},
|
||||
{
|
||||
name: "view error",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
},
|
||||
isTTY: true,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
err: errTestAutolinkClientView,
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{},
|
||||
expectedErr: errTestAutolinkClientView,
|
||||
expectedErrMsg: "error deleting autolink: autolink client view error",
|
||||
},
|
||||
{
|
||||
name: "delete error",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
},
|
||||
isTTY: true,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{
|
||||
err: errTestAutolinkClientDelete,
|
||||
},
|
||||
prompterStubs: func(pm *prompter.PrompterMock) {
|
||||
pm.ConfirmDeletionFunc = func(_ string) error {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
expectedErr: errTestAutolinkClientDelete,
|
||||
expectedErrMsg: errTestAutolinkClientDelete.Error(),
|
||||
wantStdout: "Autolink 123 has key prefix TICKET-.\n",
|
||||
},
|
||||
{
|
||||
name: "no TTY",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
Confirmed: true,
|
||||
},
|
||||
isTTY: false,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
}},
|
||||
stubDeleter: stubAutolinkDeleter{},
|
||||
wantStdout: "",
|
||||
},
|
||||
{
|
||||
name: "no TTY view error",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
},
|
||||
isTTY: false,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
err: errTestAutolinkClientView,
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{},
|
||||
expectedErr: errTestAutolinkClientView,
|
||||
expectedErrMsg: "error deleting autolink: autolink client view error",
|
||||
},
|
||||
{
|
||||
name: "no TTY delete error",
|
||||
opts: &deleteOptions{
|
||||
ID: "123",
|
||||
Confirmed: true,
|
||||
},
|
||||
isTTY: false,
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 123,
|
||||
KeyPrefix: "TICKET-",
|
||||
URLTemplate: "https://example.com/TICKET?query=<num>",
|
||||
IsAlphanumeric: true,
|
||||
},
|
||||
},
|
||||
stubDeleter: stubAutolinkDeleter{
|
||||
err: errTestAutolinkClientDelete,
|
||||
},
|
||||
expectedErr: errTestAutolinkClientDelete,
|
||||
expectedErrMsg: errTestAutolinkClientDelete.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
|
||||
ios.SetStdinTTY(tt.isTTY)
|
||||
ios.SetStdoutTTY(tt.isTTY)
|
||||
|
||||
opts := tt.opts
|
||||
opts.IO = ios
|
||||
opts.Browser = &browser.Stub{}
|
||||
|
||||
opts.IO = ios
|
||||
opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
|
||||
|
||||
opts.AutolinkDeleteClient = &tt.stubDeleter
|
||||
opts.AutolinkViewClient = &tt.stubViewer
|
||||
|
||||
pm := &prompter.PrompterMock{}
|
||||
if tt.prompterStubs != nil {
|
||||
tt.prompterStubs(pm)
|
||||
}
|
||||
tt.opts.Prompter = pm
|
||||
|
||||
err := deleteRun(opts)
|
||||
|
||||
if tt.expectedErr != nil {
|
||||
require.Error(t, err, "expected error but got none")
|
||||
assert.ErrorIs(t, err, tt.expectedErr, "unexpected error")
|
||||
assert.Equal(t, tt.expectedErrMsg, err.Error(), "unexpected error message")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
||||
})
|
||||
}
|
||||
}
|
||||
37
pkg/cmd/repo/autolink/delete/http.go
Normal file
37
pkg/cmd/repo/autolink/delete/http.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
)
|
||||
|
||||
type AutolinkDeleter struct {
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func (a *AutolinkDeleter) Delete(repo ghrepo.Interface, id string) error {
|
||||
path := fmt.Sprintf("repos/%s/%s/autolinks/%s", repo.RepoOwner(), repo.RepoName(), id)
|
||||
url := ghinstance.RESTPrefix(repo.RepoHost()) + path
|
||||
req, err := http.NewRequest(http.MethodDelete, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := a.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return fmt.Errorf("error deleting autolink: HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/%s)", path)
|
||||
} else if resp.StatusCode > 299 {
|
||||
return api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
73
pkg/cmd/repo/autolink/delete/http_test.go
Normal file
73
pkg/cmd/repo/autolink/delete/http_test.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAutolinkDeleter_Delete(t *testing.T) {
|
||||
repo := ghrepo.New("OWNER", "REPO")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
stubStatus int
|
||||
stubRespJSON string
|
||||
|
||||
expectErr bool
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "204 successful delete",
|
||||
id: "123",
|
||||
stubStatus: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
name: "404 repo or autolink not found",
|
||||
id: "123",
|
||||
stubStatus: http.StatusNotFound,
|
||||
stubRespJSON: `{}`, // API response not used in output
|
||||
expectErr: true,
|
||||
expectedErrMsg: "error deleting autolink: HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/repos/OWNER/REPO/autolinks/123)",
|
||||
},
|
||||
{
|
||||
name: "500 unexpected error",
|
||||
id: "123",
|
||||
stubRespJSON: `{"messsage": "arbitrary error"}`,
|
||||
stubStatus: http.StatusInternalServerError,
|
||||
expectErr: true,
|
||||
expectedErrMsg: "HTTP 500 (https://api.github.com/repos/OWNER/REPO/autolinks/123)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
reg.Register(
|
||||
httpmock.REST(
|
||||
http.MethodDelete,
|
||||
fmt.Sprintf("repos/%s/%s/autolinks/%s", repo.RepoOwner(), repo.RepoName(), tt.id),
|
||||
),
|
||||
httpmock.StatusJSONResponse(tt.stubStatus, tt.stubRespJSON),
|
||||
)
|
||||
defer reg.Verify(t)
|
||||
|
||||
autolinkDeleter := &AutolinkDeleter{
|
||||
HTTPClient: &http.Client{Transport: reg},
|
||||
}
|
||||
|
||||
err := autolinkDeleter.Delete(repo, tt.id)
|
||||
|
||||
if tt.expectErr {
|
||||
require.EqualError(t, err, tt.expectedErrMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ func TestAutolinkLister_List(t *testing.T) {
|
|||
name: "no autolinks",
|
||||
repo: ghrepo.New("OWNER", "REPO"),
|
||||
resp: []shared.Autolink{},
|
||||
status: 200,
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "two autolinks",
|
||||
|
|
@ -42,12 +42,12 @@ func TestAutolinkLister_List(t *testing.T) {
|
|||
URLTemplate: "https://example2.com",
|
||||
},
|
||||
},
|
||||
status: 200,
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "http error",
|
||||
repo: ghrepo.New("OWNER", "REPO"),
|
||||
status: 404,
|
||||
status: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ func TestAutolinkLister_List(t *testing.T) {
|
|||
HTTPClient: &http.Client{Transport: reg},
|
||||
}
|
||||
autolinks, err := autolinkLister.List(tt.repo)
|
||||
if tt.status == 404 {
|
||||
if tt.status == http.StatusNotFound {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "error getting autolinks: HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/repos/OWNER/REPO/autolinks)", err.Error())
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -89,8 +89,14 @@ func listRun(opts *listOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
if len(autolinks) == 0 {
|
||||
return cmdutil.NewNoResultsError(fmt.Sprintf("no autolinks found in %s", ghrepo.FullName(repo)))
|
||||
return cmdutil.NewNoResultsError(
|
||||
fmt.Sprintf(
|
||||
"no autolinks found in %s",
|
||||
cs.Bold(ghrepo.FullName(repo))),
|
||||
)
|
||||
}
|
||||
|
||||
if opts.Exporter != nil {
|
||||
|
|
@ -98,14 +104,16 @@ func listRun(opts *listOptions) error {
|
|||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
title := listHeader(ghrepo.FullName(repo), len(autolinks))
|
||||
title := fmt.Sprintf(
|
||||
"Showing %s in %s",
|
||||
text.Pluralize(len(autolinks), "autolink reference"),
|
||||
cs.Bold(ghrepo.FullName(repo)),
|
||||
)
|
||||
fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title)
|
||||
}
|
||||
|
||||
tp := tableprinter.New(opts.IO, tableprinter.WithHeader("ID", "KEY PREFIX", "URL TEMPLATE", "ALPHANUMERIC"))
|
||||
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
for _, autolink := range autolinks {
|
||||
tp.AddField(cs.Cyanf("%d", autolink.ID))
|
||||
tp.AddField(autolink.KeyPrefix)
|
||||
|
|
@ -116,7 +124,3 @@ func listRun(opts *listOptions) error {
|
|||
|
||||
return tp.Render()
|
||||
}
|
||||
|
||||
func listHeader(repoName string, count int) string {
|
||||
return fmt.Sprintf("Showing %s in %s", text.Pluralize(count, "autolink reference"), repoName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,12 +96,12 @@ func TestNewCmdList(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type stubAutoLinkLister struct {
|
||||
type stubAutolinkLister struct {
|
||||
autolinks []shared.Autolink
|
||||
err error
|
||||
}
|
||||
|
||||
func (g stubAutoLinkLister) List(repo ghrepo.Interface) ([]shared.Autolink, error) {
|
||||
func (g stubAutolinkLister) List(repo ghrepo.Interface) ([]shared.Autolink, error) {
|
||||
return g.autolinks, g.err
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ func TestListRun(t *testing.T) {
|
|||
name string
|
||||
opts *listOptions
|
||||
isTTY bool
|
||||
stubLister stubAutoLinkLister
|
||||
stubLister stubAutolinkLister
|
||||
expectedErr error
|
||||
wantStdout string
|
||||
wantStderr string
|
||||
|
|
@ -125,7 +125,7 @@ func TestListRun(t *testing.T) {
|
|||
name: "list tty",
|
||||
opts: &listOptions{},
|
||||
isTTY: true,
|
||||
stubLister: stubAutoLinkLister{
|
||||
stubLister: stubAutolinkLister{
|
||||
autolinks: []shared.Autolink{
|
||||
{
|
||||
ID: 1,
|
||||
|
|
@ -161,7 +161,7 @@ func TestListRun(t *testing.T) {
|
|||
}(),
|
||||
},
|
||||
isTTY: true,
|
||||
stubLister: stubAutoLinkLister{
|
||||
stubLister: stubAutolinkLister{
|
||||
autolinks: []shared.Autolink{
|
||||
{
|
||||
ID: 1,
|
||||
|
|
@ -184,7 +184,7 @@ func TestListRun(t *testing.T) {
|
|||
name: "list non-tty",
|
||||
opts: &listOptions{},
|
||||
isTTY: false,
|
||||
stubLister: stubAutoLinkLister{
|
||||
stubLister: stubAutolinkLister{
|
||||
autolinks: []shared.Autolink{
|
||||
{
|
||||
ID: 1,
|
||||
|
|
@ -210,7 +210,7 @@ func TestListRun(t *testing.T) {
|
|||
name: "no results",
|
||||
opts: &listOptions{},
|
||||
isTTY: true,
|
||||
stubLister: stubAutoLinkLister{
|
||||
stubLister: stubAutolinkLister{
|
||||
autolinks: []shared.Autolink{},
|
||||
},
|
||||
expectedErr: cmdutil.NewNoResultsError("no autolinks found in OWNER/REPO"),
|
||||
|
|
@ -220,7 +220,7 @@ func TestListRun(t *testing.T) {
|
|||
name: "client error",
|
||||
opts: &listOptions{},
|
||||
isTTY: true,
|
||||
stubLister: stubAutoLinkLister{
|
||||
stubLister: stubAutolinkLister{
|
||||
autolinks: []shared.Autolink{},
|
||||
err: testAutolinkClientListError{},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func (a *AutolinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolin
|
|||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("HTTP 404: Either no autolink with this ID exists for this repository or you are missing admin rights to the repository. (https://api.github.com/%s)", path)
|
||||
return nil, fmt.Errorf("HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/%s)", path)
|
||||
} else if resp.StatusCode > 299 {
|
||||
return nil, api.HandleHTTPError(resp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func TestAutolinkViewer_View(t *testing.T) {
|
|||
{
|
||||
name: "200 successful alphanumeric view",
|
||||
id: "123",
|
||||
stubStatus: 200,
|
||||
stubStatus: http.StatusOK,
|
||||
stubRespJSON: `{
|
||||
"id": 123,
|
||||
"key_prefix": "TICKET-",
|
||||
|
|
@ -45,7 +45,7 @@ func TestAutolinkViewer_View(t *testing.T) {
|
|||
{
|
||||
name: "200 successful numeric view",
|
||||
id: "123",
|
||||
stubStatus: 200,
|
||||
stubStatus: http.StatusOK,
|
||||
stubRespJSON: `{
|
||||
"id": 123,
|
||||
"key_prefix": "TICKET-",
|
||||
|
|
@ -62,14 +62,14 @@ func TestAutolinkViewer_View(t *testing.T) {
|
|||
{
|
||||
name: "404 repo or autolink not found",
|
||||
id: "123",
|
||||
stubStatus: 404,
|
||||
stubStatus: http.StatusNotFound,
|
||||
stubRespJSON: `{
|
||||
"message": "Not Found",
|
||||
"documentation_url": "https://docs.github.com/rest/repos/autolinks#get-an-autolink-reference-of-a-repository",
|
||||
"status": "404"
|
||||
}`,
|
||||
expectErr: true,
|
||||
expectedErrMsg: "HTTP 404: Either no autolink with this ID exists for this repository or you are missing admin rights to the repository. (https://api.github.com/repos/OWNER/REPO/autolinks/123)",
|
||||
expectedErrMsg: "HTTP 404: Perhaps you are missing admin rights to the repository? (https://api.github.com/repos/OWNER/REPO/autolinks/123)",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -85,11 +85,11 @@ func TestAutolinkViewer_View(t *testing.T) {
|
|||
)
|
||||
defer reg.Verify(t)
|
||||
|
||||
autolinkCreator := &AutolinkViewer{
|
||||
autolinkViewer := &AutolinkViewer{
|
||||
HTTPClient: &http.Client{Transport: reg},
|
||||
}
|
||||
|
||||
autolink, err := autolinkCreator.View(repo, tt.id)
|
||||
autolink, err := autolinkViewer.View(repo, tt.id)
|
||||
|
||||
if tt.expectErr {
|
||||
require.EqualError(t, err, tt.expectedErrMsg)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ func viewRun(opts *viewOptions) error {
|
|||
return opts.Exporter.Write(opts.IO, autolink)
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "Autolink in %s\n\n", ghrepo.FullName(repo))
|
||||
fmt.Fprintf(out, "Autolink in %s\n\n", cs.Bold(ghrepo.FullName(repo)))
|
||||
|
||||
fmt.Fprint(out, cs.Bold("ID: "))
|
||||
fmt.Fprintln(out, cs.Cyanf("%d", autolink.ID))
|
||||
|
|
|
|||
|
|
@ -49,13 +49,12 @@ func TestNewCmdView(t *testing.T) {
|
|||
{
|
||||
name: "json flag",
|
||||
input: "123 --json id",
|
||||
output: viewOptions{},
|
||||
output: viewOptions{ID: "123"},
|
||||
wantExporter: true,
|
||||
},
|
||||
{
|
||||
name: "invalid json flag",
|
||||
input: "123 --json invalid",
|
||||
output: viewOptions{},
|
||||
wantErr: true,
|
||||
errMsg: "Unknown JSON field: \"invalid\"\nAvailable fields:\n id\n isAlphanumeric\n keyPrefix\n urlTemplate",
|
||||
},
|
||||
|
|
@ -91,17 +90,18 @@ func TestNewCmdView(t *testing.T) {
|
|||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantExporter, gotOpts.Exporter != nil)
|
||||
assert.Equal(t, tt.output.ID, gotOpts.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubAutoLinkViewer struct {
|
||||
type stubAutolinkViewer struct {
|
||||
autolink *shared.Autolink
|
||||
err error
|
||||
}
|
||||
|
||||
func (g stubAutoLinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, error) {
|
||||
func (g stubAutolinkViewer) View(repo ghrepo.Interface, id string) (*shared.Autolink, error) {
|
||||
return g.autolink, g.err
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ func TestViewRun(t *testing.T) {
|
|||
tests := []struct {
|
||||
name string
|
||||
opts *viewOptions
|
||||
stubViewer stubAutoLinkViewer
|
||||
stubViewer stubAutolinkViewer
|
||||
expectedErr error
|
||||
wantStdout string
|
||||
}{
|
||||
|
|
@ -124,7 +124,7 @@ func TestViewRun(t *testing.T) {
|
|||
opts: &viewOptions{
|
||||
ID: "1",
|
||||
},
|
||||
stubViewer: stubAutoLinkViewer{
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 1,
|
||||
KeyPrefix: "TICKET-",
|
||||
|
|
@ -150,7 +150,7 @@ func TestViewRun(t *testing.T) {
|
|||
return exporter
|
||||
}(),
|
||||
},
|
||||
stubViewer: stubAutoLinkViewer{
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: &shared.Autolink{
|
||||
ID: 1,
|
||||
KeyPrefix: "TICKET-",
|
||||
|
|
@ -163,7 +163,7 @@ func TestViewRun(t *testing.T) {
|
|||
{
|
||||
name: "client error",
|
||||
opts: &viewOptions{},
|
||||
stubViewer: stubAutoLinkViewer{
|
||||
stubViewer: stubAutolinkViewer{
|
||||
autolink: nil,
|
||||
err: testAutolinkClientViewError{},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue